diff --git a/Eatery Blue.xcodeproj/project.pbxproj b/Eatery Blue.xcodeproj/project.pbxproj index 2053b599..9eee239c 100644 --- a/Eatery Blue.xcodeproj/project.pbxproj +++ b/Eatery Blue.xcodeproj/project.pbxproj @@ -110,7 +110,6 @@ 5DCB869C2782396000134066 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DCB869B2782396000134066 /* main.swift */; }; 5DCB86A22783544C00134066 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DCB86A12783544C00134066 /* LocationManager.swift */; }; 5DCB86A427835EF900134066 /* DummyLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DCB86A327835EF900134066 /* DummyLocationManager.swift */; }; - 5DDAD4732773D57A00CC98DD /* DummyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDAD4722773D57A00CC98DD /* DummyData.swift */; }; 5DDAD4752773D70900CC98DD /* EateryMediumCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDAD4742773D70900CC98DD /* EateryMediumCardView.swift */; }; 5DDAD4772773DA9800CC98DD /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDAD4762773DA9800CC98DD /* RootViewController.swift */; }; 5DDAD47D2773E8BA00CC98DD /* PillFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDAD47C2773E8BA00CC98DD /* PillFiltersView.swift */; }; @@ -162,6 +161,7 @@ BFD955952900A8150049D77C /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD955942900A8150049D77C /* UIColor+Extension.swift */; }; C31EC93B2CF3DEBB00B2E99F /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = C31EC93A2CF3DEBB00B2E99F /* FirebaseMessaging */; }; D00DC9592BB370520051D3EC /* CompareMenusTabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00DC9582BB370520051D3EC /* CompareMenusTabsViewController.swift */; }; + D00FEC9A2F4BF4DB0081CEE0 /* DateDecodingStrategy+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00FEC992F4BF4CF0081CEE0 /* DateDecodingStrategy+Extension.swift */; }; D01D80772BC638C900253976 /* CompareMenusInternalOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01D80762BC638C900253976 /* CompareMenusInternalOnboardingView.swift */; }; D03A749B2B92BE9500C14083 /* CompareMenusButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03A749A2B92BE9500C14083 /* CompareMenusButton.swift */; }; D04FB8792BCEE7820058F732 /* SettingsAppIconSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FB8782BCEE7820058F732 /* SettingsAppIconSheetViewController.swift */; }; @@ -173,6 +173,7 @@ D053BF622CCDAA250060F82C /* EateryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053BF612CCDAA250060F82C /* EateryListView.swift */; }; D0548AD72BB5201B0064489D /* ScrollHitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0548AD62BB5201B0064489D /* ScrollHitView.swift */; }; D06230E82CB9D4A70092C04A /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06230E72CB9D4A70092C04A /* FavoritesViewController.swift */; }; + D063DB782F36942E00740573 /* EateryType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D063DB772F36941C00740573 /* EateryType+Extension.swift */; }; D064656D2C8CE634001902A4 /* NotificationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D064656C2C8CE634001902A4 /* NotificationButton.swift */; }; D06AE51C2CE1CC4500C0AFF5 /* TabButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06AE51B2CE1CC3700C0AFF5 /* TabButtonView.swift */; }; D0730BB82D2B49AB00F9070A /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0730BB72D2B49A600F9070A /* HomeViewController.swift */; }; @@ -309,7 +310,6 @@ 5DCB869B2782396000134066 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 5DCB86A12783544C00134066 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; 5DCB86A327835EF900134066 /* DummyLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyLocationManager.swift; sourceTree = ""; }; - 5DDAD4722773D57A00CC98DD /* DummyData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyData.swift; sourceTree = ""; }; 5DDAD4742773D70900CC98DD /* EateryMediumCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EateryMediumCardView.swift; sourceTree = ""; }; 5DDAD4762773DA9800CC98DD /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 5DDAD47C2773E8BA00CC98DD /* PillFiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillFiltersView.swift; sourceTree = ""; }; @@ -359,6 +359,7 @@ BFD955942900A8150049D77C /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; C31EC9392CF3DC0800B2E99F /* Eatery Blue.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Eatery Blue.entitlements"; sourceTree = ""; }; D00DC9582BB370520051D3EC /* CompareMenusTabsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareMenusTabsViewController.swift; sourceTree = ""; }; + D00FEC992F4BF4CF0081CEE0 /* DateDecodingStrategy+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDecodingStrategy+Extension.swift"; sourceTree = ""; }; D01D80762BC638C900253976 /* CompareMenusInternalOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareMenusInternalOnboardingView.swift; sourceTree = ""; }; D03A749A2B92BE9500C14083 /* CompareMenusButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareMenusButton.swift; sourceTree = ""; }; D04FB8782BCEE7820058F732 /* SettingsAppIconSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppIconSheetViewController.swift; sourceTree = ""; }; @@ -369,6 +370,7 @@ D053BF612CCDAA250060F82C /* EateryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EateryListView.swift; sourceTree = ""; }; D0548AD62BB5201B0064489D /* ScrollHitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollHitView.swift; sourceTree = ""; }; D06230E72CB9D4A70092C04A /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; + D063DB772F36941C00740573 /* EateryType+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EateryType+Extension.swift"; sourceTree = ""; }; D064656C2C8CE634001902A4 /* NotificationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButton.swift; sourceTree = ""; }; D06AE51B2CE1CC3700C0AFF5 /* TabButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabButtonView.swift; sourceTree = ""; }; D0730BB72D2B49A600F9070A /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; @@ -658,7 +660,6 @@ 5DCB869F27823DDB00134066 /* Testing */ = { isa = PBXGroup; children = ( - 5DDAD4722773D57A00CC98DD /* DummyData.swift */, 5DCB86A327835EF900134066 /* DummyLocationManager.swift */, ); path = Testing; @@ -687,6 +688,8 @@ 5DF304B327797BD200E2C9A4 /* Extensions */ = { isa = PBXGroup; children = ( + D00FEC992F4BF4CF0081CEE0 /* DateDecodingStrategy+Extension.swift */, + D063DB772F36941C00740573 /* EateryType+Extension.swift */, D0B499132D6D17460044C856 /* UIView+Extension.swift */, D0B499112D6D10AD0044C856 /* UIViewController+Extension.swift */, D0A7A3072D3419BA00D9B472 /* NSAttributedString+Extension.swift */, @@ -1169,7 +1172,6 @@ 5DF5F7EB2774D8FE001ADCDC /* EateryViewController.swift in Sources */, 5DF304A527791D2C00E2C9A4 /* EateryModelController.swift in Sources */, 5DF938D1277662CC008ED0A1 /* PaymentMethodFilterView.swift in Sources */, - 5DDAD4732773D57A00CC98DD /* DummyData.swift in Sources */, 5D1F13B2277638D4001A21B9 /* SheetPresentationController.swift in Sources */, 5DF938D327768B84008ED0A1 /* SheetViewController.swift in Sources */, FD88925A2AE9B33C0084EDAD /* EateryMemoryCache.swift in Sources */, @@ -1185,6 +1187,7 @@ D0730BB82D2B49AB00F9070A /* HomeViewController.swift in Sources */, 5D73C37F2780D5850020507C /* HomeSearchEmptyModelController.swift in Sources */, 5D34F090279B48CE000F9056 /* EateryCardAlertView.swift in Sources */, + D00FEC9A2F4BF4DB0081CEE0 /* DateDecodingStrategy+Extension.swift in Sources */, 5DF304CB277A36DE00E2C9A4 /* IssueTypeButtonView.swift in Sources */, D0B499122D6D10B10044C856 /* UIViewController+Extension.swift in Sources */, 5DF304C6277A22AD00E2C9A4 /* ReportIssueButtonView.swift in Sources */, @@ -1214,6 +1217,7 @@ 5D468A00277D146500986AFF /* ClearTableViewCell.swift in Sources */, D0A7A3082D3419C300D9B472 /* NSAttributedString+Extension.swift in Sources */, D09D20A52B96DE19004C72ED /* CompareMenusNavigationView.swift in Sources */, + D063DB782F36942E00740573 /* EateryType+Extension.swift in Sources */, D0A7A30A2D342E1800D9B472 /* MenusViewController.swift in Sources */, 5D468A15277E3FDC00986AFF /* Networking.swift in Sources */, 5D0CDC4F2774FDAE00AD0E27 /* MenuHeaderView.swift in Sources */, @@ -1489,7 +1493,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.3; + MARKETING_VERSION = 3.4; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.cuappdev.eatery; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1532,7 +1536,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.3; + MARKETING_VERSION = 3.4; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.cuappdev.eatery; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Eatery Blue/AppDelegate.swift b/Eatery Blue/AppDelegate.swift index 0d35ba6e..4f21b483 100644 --- a/Eatery Blue/AppDelegate.swift +++ b/Eatery Blue/AppDelegate.swift @@ -35,6 +35,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD _ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + // Check for updates + Task { + do { + let serverVersion = try await Networking.default.getAppVersion() + let deviceVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0" + + if shouldForceUpdate(device: deviceVersion, server: serverVersion) { + await MainActor.run { + showForceUpdateAlert( + appStoreURL: "https://apps.apple.com/us/app/eatery-cornell-dining/id1089672962" + ) + } + } + } catch { + print("Version check failed: \(error)") + } + } + FirebaseApp.configure() // Request notification permissions @@ -126,4 +144,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Logger.notifications.info("Notification tapped: \(response.notification.request.content.userInfo)") completionHandler() } + + // MARK: - Helper functions + + func shouldForceUpdate(device: String, server: String) -> Bool { + let parse = { (v: String) in v.split(separator: ".").prefix(2).compactMap { Int($0) } } + + let d = parse(device) + let s = parse(server) + + // Compare arrays: [3, 4] > [3, 3] + return s.lexicographicallyPrecedes(d) == false && s != d + } + + @MainActor + private func showForceUpdateAlert(appStoreURL: String) { + let alert = UIAlertController( + title: "Update Required", + message: "A new version of Eatery is available.", + preferredStyle: .alert + ) + + let updateAction = UIAlertAction(title: "Update Now", style: .default) { _ in + if let url = URL(string: appStoreURL) { + UIApplication.shared.open(url) + } + + self.showForceUpdateAlert(appStoreURL: appStoreURL) + } + + alert.addAction(updateAction) + + // Find the current active window's rootViewController + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController { + // Find the top-most controller (in case a modal is already up) + var topController = rootVC + while let presented = topController.presentedViewController { + topController = presented + } + topController.present(alert, animated: true) + } + } } diff --git a/Eatery Blue/Extensions/DateDecodingStrategy+Extension.swift b/Eatery Blue/Extensions/DateDecodingStrategy+Extension.swift new file mode 100644 index 00000000..4975655b --- /dev/null +++ b/Eatery Blue/Extensions/DateDecodingStrategy+Extension.swift @@ -0,0 +1,30 @@ +// +// DateDecodingStrategy+Extension.swift +// Eatery Blue +// +// Created by Peter Bidoshi on 2/22/26. +// + +import Foundation + +extension JSONDecoder.DateDecodingStrategy { + static let fracSecondsISO8601 = custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + let formatter = ISO8601DateFormatter() + + // Strategy 1: Try with milliseconds + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = formatter.date(from: dateString) { return date } + + // Strategy 2: Try without milliseconds + formatter.formatOptions = [.withInternetDateTime] + if let date = formatter.date(from: dateString) { return date } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unrecognized date format: \(dateString)" + ) + } +} diff --git a/Eatery Blue/Extensions/EateryType+Extension.swift b/Eatery Blue/Extensions/EateryType+Extension.swift new file mode 100644 index 00000000..5bf8e848 --- /dev/null +++ b/Eatery Blue/Extensions/EateryType+Extension.swift @@ -0,0 +1,28 @@ +// +// EateryType+Extension.swift +// Eatery Blue +// +// Created by Peter Bidoshi on 2/6/26. +// + +import EateryModel + +extension Array where Element == EateryType { + func validated() -> [EateryType]? { + guard count > 0 else { return nil } + return self + } + + func formatted() -> String { + // should be "{0}, {1}, and {2}" + let count = self.count + if count == 1 { + return self[0].description + } else if count == 2 { + return "\(self[0].description) and \(self[1].description)" + } else { + let allButLast = self[0 ..< (count - 1)].map { $0.description }.joined(separator: ", ") + return "\(allButLast), and \(self[count - 1].description)" + } + } +} diff --git a/Eatery Blue/Extensions/String+Extension.swift b/Eatery Blue/Extensions/String+Extension.swift index 38e33d65..255ee209 100644 --- a/Eatery Blue/Extensions/String+Extension.swift +++ b/Eatery Blue/Extensions/String+Extension.swift @@ -5,35 +5,21 @@ // Created by Jayson Hahn on 10/17/23. // +import EateryModel import Foundation extension String { - static let Eatery = String() - /** - Determines the correct meal based on the time of day. - - This function gets the time of day from Date and returns a string of the meal type. If it is a specific meal time, - that meal time is returned, and if it is not a specific meal time, the next meal time is returned. + Determines if a string has meaningful characters (not just whitespace or empty string). - - Returns: If it is 10:30 pm - 10:29 am inclusive, breakfest is returned. If it is 10:30 am - 2:29 pm inclusive, - lunch is returned. If it is 2:30 pm - 8:29 pm inclusive, dinner is returned. - If it is 8:30 pm - 10:29 pm inclusive, late dinner is returned. + - Returns: `nil` if the string is empty or only contains whitespace/newline characters. + Otherwise, returns the original string. */ - func mealFromTime() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH" - guard let hour = Int(dateFormatter.string(from: Date())) else { return "Breakfast" } - dateFormatter.dateFormat = "mm" - guard let minute = Int(dateFormatter.string(from: Date())) else { return "Breakfast" } - if hour < 10 || hour == 10 && minute < 30 || hour >= 23 || hour == 22 && minute >= 30 { - return "Breakfast" - } else if hour == 10 && minute >= 30 || hour > 10 && hour < 14 || hour == 14 && minute < 30 { - return "Lunch" - } else if hour == 14 && minute >= 30 || hour > 14 && hour < 20 || hour == 20 && minute < 30 { - return "Dinner" - } else { - return "Late Dinner" + func validated() -> String? { + if trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return nil } + + return self } } diff --git a/Eatery Blue/Model/CoreDataStack.swift b/Eatery Blue/Model/CoreDataStack.swift index d1db808f..230d0881 100644 --- a/Eatery Blue/Model/CoreDataStack.swift +++ b/Eatery Blue/Model/CoreDataStack.swift @@ -38,7 +38,7 @@ class CoreDataStack { } } - func metadata(eateryId: Int64) -> EateryMetadata { + func metadata(eateryId: Int) -> EateryMetadata { let fetchRequest = NSFetchRequest() fetchRequest.entity = EateryMetadata.entity() fetchRequest.predicate = NSPredicate(format: "eateryId == %d", eateryId) @@ -49,7 +49,7 @@ class CoreDataStack { } else { let metadata = EateryMetadata(context: context) - metadata.eateryId = eateryId + metadata.eateryId = Int64(eateryId) save() return metadata } diff --git a/Eatery Blue/Model/EateryFilter.swift b/Eatery Blue/Model/EateryFilter.swift index 41ed63cb..71fc30fb 100644 --- a/Eatery Blue/Model/EateryFilter.swift +++ b/Eatery Blue/Model/EateryFilter.swift @@ -51,7 +51,7 @@ struct EateryFilter: Codable { if brbsEnabled || mealSwipesEnabled { return .or([ brbsEnabled ? .acceptsPaymentMethod(.brbs) : .false, - mealSwipesEnabled ? .acceptsPaymentMethod(.mealSwipes) : .false + mealSwipesEnabled ? .acceptsPaymentMethod(.mealSwipe) : .false ]) } else { return .true @@ -69,9 +69,9 @@ struct EateryFilter: Codable { func campusAreaPredicate() -> EateryPredicate { if north || west || central { return .or([ - north ? .campusArea("North") : .false, - west ? .campusArea("West") : .false, - central ? .campusArea("Central") : .false + north ? .campusArea(.north) : .false, + west ? .campusArea(.west) : .false, + central ? .campusArea(.central) : .false ]) } else { return .true diff --git a/Eatery Blue/Model/EateryFormatter.swift b/Eatery Blue/Model/EateryFormatter.swift index 4b906183..c03e019c 100644 --- a/Eatery Blue/Model/EateryFormatter.swift +++ b/Eatery Blue/Model/EateryFormatter.swift @@ -60,7 +60,7 @@ class EateryFormatter { func formatStatus(_ status: EateryStatus) -> NSAttributedString { switch status { case let .open(event): - let timeString = timeFormatter.string(from: event.endDate) + let timeString = timeFormatter.string(from: event.endTimestamp) return NSAttributedString( string: "Open until \(timeString)", attributes: [.foregroundColor: UIColor.Eatery.green as Any] @@ -73,14 +73,14 @@ class EateryFormatter { ) case let .openingSoon(event): - let timeString = timeFormatter.string(from: event.startDate) + let timeString = timeFormatter.string(from: event.startTimestamp) return NSAttributedString( string: "Opening at \(timeString)", attributes: [.foregroundColor: UIColor.Eatery.orange as Any] ) case let .closingSoon(event): - let timeString = timeFormatter.string(from: event.endDate) + let timeString = timeFormatter.string(from: event.endTimestamp) return NSAttributedString( string: "Closing at \(timeString)", attributes: [.foregroundColor: UIColor.Eatery.orange as Any] @@ -93,7 +93,7 @@ class EateryFormatter { } func formatEventTime(_ event: Event) -> String { - "\(timeFormatter.string(from: event.startDate)) - \(timeFormatter.string(from: event.endDate))" + "\(timeFormatter.string(from: event.startTimestamp)) - \(timeFormatter.string(from: event.endTimestamp))" } func formatEventTimes(_ events: [Event]) -> String { @@ -110,12 +110,12 @@ class EateryFormatter { switch eatery.status { case let .closingSoon(event): return NSAttributedString( - string: "Open until \(timeFormatter.string(from: event.endDate))", + string: "Open until \(timeFormatter.string(from: event.endTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.orange as Any] ) case let .open(event): return NSAttributedString( - string: "Open until \(timeFormatter.string(from: event.endDate))", + string: "Open until \(timeFormatter.string(from: event.endTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.green as Any] ) default: @@ -128,12 +128,12 @@ class EateryFormatter { if let nextEventOfDay = EateryStatus.nextEvent(eatery.events, date: date, on: day) { if EateryStatus.previousEvent(eatery.events, date: date, on: day) != nil { return NSAttributedString( - string: "Re-opens at \(timeFormatter.string(from: nextEventOfDay.startDate))", + string: "Re-opens at \(timeFormatter.string(from: nextEventOfDay.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.orange as Any] ) } else { return NSAttributedString( - string: "Opens at \(timeFormatter.string(from: nextEventOfDay.startDate))", + string: "Opens at \(timeFormatter.string(from: nextEventOfDay.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.orange as Any] ) } @@ -141,12 +141,12 @@ class EateryFormatter { date: date, on: day.advanced(by: 1)) { return NSAttributedString( - string: "Closed until \(timeFormatter.string(from: nextEventOfNextDay.startDate))", + string: "Closed until \(timeFormatter.string(from: nextEventOfNextDay.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.red as Any] ) } else if let nextEvent = EateryStatus.nextEvent(eatery.events, date: date) { return NSAttributedString( - string: "Closed until \(mediumDayMonthFormatter.string(from: nextEvent.startDate))", + string: "Closed until \(mediumDayMonthFormatter.string(from: nextEvent.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.red as Any] ) } else { @@ -161,29 +161,29 @@ class EateryFormatter { private func firstLineSecondComponent(_ eatery: Eatery, date: Date) -> NSAttributedString? { let day = Day(date: date) if eatery.isOpen { - if eatery.paymentMethods.contains(.mealSwipes) { + if eatery.paymentMethods.contains(.mealSwipe) { return NSAttributedString( string: "Meal swipes allowed", attributes: [.foregroundColor: UIColor.Eatery.blue as Any] ) - } else if eatery.paymentMethods == [.cash, .credit] { + } else if eatery.paymentMethods == [.cash, .card] { return NSAttributedString( string: "Cash or credit only", attributes: [.foregroundColor: UIColor.Eatery.green as Any] ) - } else if let menuSummary = eatery.menuSummary { - return NSAttributedString(string: menuSummary) + } else if let eateryTypes = eatery.eateryTypes.validated() { + return NSAttributedString(string: eateryTypes.formatted()) } else { return nil } } else if let nextEventOfNextDay = EateryStatus.nextEvent(eatery.events, date: date, on: day.advanced(by: 1)) { return NSAttributedString( - string: "Closed until \(timeFormatter.string(from: nextEventOfNextDay.startDate))", + string: "Closed until \(timeFormatter.string(from: nextEventOfNextDay.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.red as Any] ) } else if let nextEvent = EateryStatus.nextEvent(eatery.events, date: date) { return NSAttributedString( - string: "Closed until \(mediumDayMonthFormatter.string(from: nextEvent.startDate))", + string: "Closed until \(mediumDayMonthFormatter.string(from: nextEvent.startTimestamp))", attributes: [.foregroundColor: UIColor.Eatery.red as Any] ) } else { @@ -226,7 +226,7 @@ class EateryFormatter { var lines: [NSAttributedString] = [] var firstLineComponents: [NSAttributedString] = [] - if let locationDescription = eatery.locationDescription { + if let locationDescription = eatery.location.validated() { firstLineComponents.append(NSAttributedString( string: locationDescription )) @@ -277,21 +277,25 @@ class EateryFormatter { func formatEateryTimeTotal( _ eatery: Eatery, userLocation: CLLocation?, - departureDate: Date + departureDate _: Date ) -> String { - let (walkTime, waitTime) = eatery.timingInfo(userLocation: userLocation, departureDate: departureDate) + let walkTime = eatery.walkTime(userLocation: userLocation) - if let waitTime = waitTime { - let minutesLow = Int(round(((walkTime ?? 0) + waitTime.low) / 60)) - let minutesHigh = Int(round(((walkTime ?? 0) + waitTime.high) / 60)) + // Wait times not supported as of backend refactor 01/29/2026 - if minutesLow > walkTimeMinutesCap + waitTimeMinutesCap { - return ">\(walkTimeMinutesCap + waitTimeMinutesCap) min" - } else { - return minutesLow < minutesHigh ? "\(minutesLow)-\(minutesHigh) min" : "\(minutesLow) min" - } +// if let waitTime = waitTime { +// let minutesLow = Int(round(((walkTime ?? 0) + waitTime.low) / 60)) +// let minutesHigh = Int(round(((walkTime ?? 0) + waitTime.high) / 60)) +// +// if minutesLow > walkTimeMinutesCap + waitTimeMinutesCap { +// return ">\(walkTimeMinutesCap + waitTimeMinutesCap) min" +// } else { +// return minutesLow < minutesHigh ? "\(minutesLow)-\(minutesHigh) min" : "\(minutesLow) min" +// } +// +// } else - } else if let walkTime = walkTime { + if let walkTime = walkTime { let minutes = Int(round(walkTime / 60)) if minutes > walkTimeMinutesCap { return ">\(walkTimeMinutesCap) min" @@ -319,51 +323,27 @@ class EateryFormatter { } } - func formatEateryWaitTime(_ eatery: Eatery, font _: UIFont, userLocation: CLLocation?, - departureDate: Date) -> String { - let (_, waitTime) = eatery.timingInfo(userLocation: userLocation, departureDate: departureDate) - - if let waitTime = waitTime { - let minutesLow = Int(round(waitTime.low / 60)) - let minutesHigh = Int(round(waitTime.high / 60)) - let avgMinutes = Int((minutesLow + minutesHigh) / 2) - - if minutesLow > walkTimeMinutesCap + waitTimeMinutesCap { - return ">\(walkTimeMinutesCap + waitTimeMinutesCap) min wait" - } else { - return "\(avgMinutes) min wait" - } - - } else { - return "-- min wait" - } - } - - func formatPaymentMethod(_ paymentMethod: PaymentMethod) -> String { - switch paymentMethod { - case .mealSwipes: return "Meal swipes" - case .brbs: return "BRBs" - case .cash: return "Cash" - case .credit: return "Credit" - } - } +// func formatEateryWaitTime(_ eatery: Eatery, font _: UIFont, userLocation: CLLocation?, +// departureDate: Date) -> String { +// let (_, waitTime) = eatery.timingInfo(userLocation: userLocation, departureDate: departureDate) +// +// if let waitTime = waitTime { +// let minutesLow = Int(round(waitTime.low / 60)) +// let minutesHigh = Int(round(waitTime.high / 60)) +// let avgMinutes = Int((minutesLow + minutesHigh) / 2) +// +// if minutesLow > walkTimeMinutesCap + waitTimeMinutesCap { +// return ">\(walkTimeMinutesCap + waitTimeMinutesCap) min wait" +// } else { +// return "\(avgMinutes) min wait" +// } +// +// } else { +// return "-- min wait" +// } +// } func formatPaymentMethods(_ paymentMethods: Set) -> String { - var components: [String] = [] - - if paymentMethods.contains(.mealSwipes) { - components.append("Meal swipes") - } - if paymentMethods.contains(.brbs) { - components.append("BRBs") - } - if paymentMethods.contains(.cash) { - components.append("Cash") - } - if paymentMethods.contains(.credit) { - components.append("Credit") - } - - return components.joined(separator: ", ") + return paymentMethods.map { $0.description }.joined(separator: ", ") } } diff --git a/Eatery Blue/Model/EateryPredicate.swift b/Eatery Blue/Model/EateryPredicate.swift index fbf4a7ec..e92ed6ab 100644 --- a/Eatery Blue/Model/EateryPredicate.swift +++ b/Eatery Blue/Model/EateryPredicate.swift @@ -19,7 +19,7 @@ indirect enum EateryPredicate { case or([EateryPredicate]) case acceptsPaymentMethod(_ paymentMethod: PaymentMethod) - case campusArea(String) + case campusArea(CampusArea) case isFavorite case isOpen case underNMinutes(_ n: Int, userLocation: CLLocation, departureDate: Date) @@ -53,7 +53,7 @@ indirect enum EateryPredicate { return predicates.contains { $0.isSatisfied(by: eatery, metadata: metadata) } case let .underNMinutes(n, userLocation, departureDate): - guard let totalTime = eatery.expectedTotalTime(userLocation: userLocation, departureDate: departureDate) + guard let totalTime = eatery.walkTime(userLocation: userLocation) else { return false } diff --git a/Eatery Blue/Networking/Networking.swift b/Eatery Blue/Networking/Networking.swift index 35f79c3f..b6e80cce 100644 --- a/Eatery Blue/Networking/Networking.swift +++ b/Eatery Blue/Networking/Networking.swift @@ -21,14 +21,17 @@ class Networking { KeychainAccess.shared.retrieveToken() ?? "" } - let simpleUrl: URL - init(fetchUrl: URL) { baseUrl = fetchUrl - let eateryApi = EateryAPI(url: fetchUrl) + let eateryApi = EateryAPI(url: fetchUrl.appendingPathComponent("eateries")) eateryCache = EateryMemoryCache(fetchAll: eateryApi.eateries) accounts = FetchAccounts() - simpleUrl = URL(string: "\(baseUrl)simple/") ?? baseUrl + } + + func getAppVersion() async throws -> String { + let eateryAPI = EateryAPI(url: baseUrl.appendingPathComponent("version")) + + return try await eateryAPI.version().version } func loadAllEatery() async throws -> [Eatery] { @@ -53,13 +56,8 @@ class Networking { return eatery } - func loadSimpleEateries() async throws -> [Eatery] { - let eateryApi = EateryAPI(url: simpleUrl) - return try await eateryApi.eateries() - } - func loadEateryByDay(day: Int) async throws -> [Eatery] { - if let url = URL(string: "\(baseUrl)day/\(day)/") { + if let url = URL(string: "\(baseUrl)?days=\(day)") { let eateryApi = EateryAPI(url: url) return try await eateryApi.eateries() } diff --git a/Eatery Blue/Testing/DummyData.swift b/Eatery Blue/Testing/DummyData.swift deleted file mode 100644 index b101e844..00000000 --- a/Eatery Blue/Testing/DummyData.swift +++ /dev/null @@ -1,396 +0,0 @@ -// -// DummyData.swift -// Eatery Blue -// -// Created by William Ma on 12/22/21. -// - -import EateryModel -import Foundation - -enum DummyData { - static let eateries = [macs, terrace, rose] - - static let macsBreakfastMenu = Menu(categories: [ - MenuCategory(category: "Meat", items: [ - MenuItem( - id: 0, - description: "A bacon strip.", - healthy: false, - name: "Bacon", - price: 625 - ), - MenuItem( - id: 1, - description: "Pork sausage link.", - healthy: false, - name: "Pork", - price: 525 - ) - ]), - MenuCategory(category: "Egg", items: [ - MenuItem( - id: 2, - description: "Scrambled egg.", - healthy: true, - name: "Egg", - price: 1100 - ) - ]) - ]) - - static let macsLunchMenu = Menu(categories: [ - MenuCategory(category: "Enchiladas", items: [ - MenuItem( - id: 3, - description: "A tortilla-wrapped chicken enchilada with your choice of toppings.", - healthy: false, - name: "Chicken", - price: 625 - ), - MenuItem( - id: 4, - description: "A tortilla-wrapped bean and corn enchilada with your choice of toppings.", - healthy: false, - name: "Bean & Corn", - price: 525 - ) - ]), - MenuCategory(category: "Salads", items: [ - MenuItem( - id: 5, - description: "A chopped salad with your choice of toppings.", - healthy: true, - name: "Chopped Salad", - price: 1100 - ) - ]) - ]) - - static let macsDinnerMenu = Menu(categories: [ - MenuCategory(category: "Grill", items: [ - MenuItem( - id: 6, - description: "Carolina BBQ Pulled Pork with Coleslaw.", - healthy: false, - name: "BBQ", - price: 625 - ), - MenuItem( - id: 7, - description: "French fries.", - healthy: false, - name: "Potatoes", - price: 525 - ) - ]), - MenuCategory(category: "Chef's Table", items: [ - MenuItem( - id: 8, - description: "Vegan tofu chili.", - healthy: true, - name: "Chili", - price: 1100 - ) - ]) - ]) - - static let macs = Eatery( - campusArea: "Central", - events: [ - Event( - canonicalDay: Day().advanced(by: 0), - description: "Breakfast", - endTimestamp: Day().advanced(by: 0).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: macsBreakfastMenu, - startTimestamp: Day().advanced(by: 0).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 1), - description: "Lunch", - endTimestamp: Day().advanced(by: 1).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: macsLunchMenu, - startTimestamp: Day().advanced(by: 1).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 2), - description: "Dinner", - endTimestamp: Day().advanced(by: 2).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: macsDinnerMenu, - startTimestamp: Day().advanced(by: 2).date(hour: 7, minute: 0).timeIntervalSince1970 - ) - ], - id: 1001, - imageUrl: nil, - latitude: nil, - locationDescription: "Statler Hall", - longitude: nil, - menuSummary: "Flatbreads, salads, pasta", - name: "Mac's Café", - onlineOrderUrl: nil, - paymentMethods: [.brbs, .cash, .credit, .mealSwipes], - waitTimesByDay: [ - Day(): WaitTimes( - samples: (0 ..< 15).map { (index: Int) -> WaitTimeSample in - WaitTimeSample( - timestamp: Date().timeIntervalSince1970 + TimeInterval(index) * 60 * 15, - low: TimeInterval(index + 1) * 60, - expected: TimeInterval(index + 2) * 60, - high: TimeInterval(index + 3) * 60 - ) - }, - samplingMethod: .nearestNeighbor - ) - ] - ) - - static let terraceBreakfastMenu = Menu(categories: [ - MenuCategory(category: "Potato", items: [ - MenuItem( - id: 9, - description: "Fried potato puffs.", - healthy: false, - name: "Potato", - price: 1025 - ) - ]), - MenuCategory(category: "Bread", items: [ - MenuItem( - id: 10, - description: "Chocolate bread pudding.", - healthy: true, - name: "Chocolate Bread", - price: 1100 - ) - ]) - ]) - - static let terraceLunchMenu = Menu(categories: [ - MenuCategory(category: "Pho Bar", items: [ - MenuItem( - id: 11, - description: "A pho bowl with your choice of toppings.", - healthy: false, - name: "Pho Bowl", - price: 1025 - ), - MenuItem( - id: 12, - description: "A rice bowl with your choice of toppings.", - healthy: false, - name: "Rice Bowl", - price: 1025 - ) - ]), - MenuCategory(category: "Salads", items: [ - MenuItem( - id: 13, - description: "A mixed salad with your choice of toppings.", - healthy: true, - name: "Mixed Salad", - price: 1100 - ) - ]) - ]) - - static let terraceDinnerMenu = Menu(categories: [ - MenuCategory(category: "Chef's Table - Sides", items: [ - MenuItem( - id: 14, - description: "Papas Bravas - spicy potatoes.", - healthy: false, - name: "Potato", - price: 1025 - ), - MenuItem( - id: 16, - description: "Steamed fresh broccoli.", - healthy: false, - name: "Broccoli", - price: 1025 - ) - ]), - MenuCategory(category: "Salads", items: [ - MenuItem( - id: 15, - description: "Traditional hummus.", - healthy: true, - name: "Hummus", - price: 1100 - ) - ]) - ]) - - static let terrace = Eatery( - campusArea: "Central", - events: [ - Event( - canonicalDay: Day().advanced(by: 0), - description: "Breakfast", - endTimestamp: Day().advanced(by: 0).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: terraceBreakfastMenu, - startTimestamp: Day().advanced(by: 0).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 1), - description: "Lunch", - endTimestamp: Day().advanced(by: 1).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: terraceLunchMenu, - startTimestamp: Day().advanced(by: 1).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 2), - description: "Dinner", - endTimestamp: Day().advanced(by: 2).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: terraceDinnerMenu, - startTimestamp: Day().advanced(by: 2).date(hour: 7, minute: 0).timeIntervalSince1970 - ) - ], - id: 1002, - imageUrl: nil, - latitude: nil, - locationDescription: "Statler Hall", - longitude: nil, - menuSummary: "Burrito bowls, pho and ramen, salads", - name: "The Terrace Restaurant", - onlineOrderUrl: nil, - paymentMethods: [.brbs, .cash, .credit, .mealSwipes], - waitTimesByDay: [ - Day(): WaitTimes( - samples: (0 ..< 15).map { (index: Int) -> WaitTimeSample in - WaitTimeSample( - timestamp: Date().timeIntervalSince1970 + TimeInterval(index) * 60 * 15, - low: TimeInterval(index + 1) * 60, - expected: TimeInterval(index + 2) * 60, - high: TimeInterval(index + 3) * 60 - ) - }, - samplingMethod: .nearestNeighbor - ) - ] - ) - - static let roseBreakfastMenu = Menu(categories: [ - MenuCategory(category: "Taco Breakfast", items: [ - MenuItem( - id: 17, - description: "Filling for your taco.", - healthy: false, - name: "Southwest chicken" - ), - MenuItem( - id: 18, - description: "Filling for your taco.", - healthy: false, - name: "Corn & Beans" - ) - ]), - MenuCategory(category: "Desserts Breakfast", items: [ - MenuItem( - id: 19, - description: "A chocolate chip cookie.", - healthy: false, - name: "Cookie" - ) - ]) - ]) - - static let roseLunchMenu = Menu(categories: [ - MenuCategory(category: "Taco Lunch", items: [ - MenuItem( - id: 20, - description: "Filling for your taco.", - healthy: false, - name: "Southwest chicken" - ), - MenuItem( - id: 21, - description: "Filling for your taco.", - healthy: false, - name: "Corn & Beans" - ) - ]), - MenuCategory(category: "Dessert Lunch", items: [ - MenuItem( - id: 22, - description: "A chocolate chip cookie.", - healthy: false, - name: "Cookie" - ) - ]) - ]) - - static let roseDinnerMenu = Menu(categories: [ - MenuCategory(category: "Taco Dinner", items: [ - MenuItem( - id: 23, - description: "Filling for your taco.", - healthy: false, - name: "Southwest chicken" - ), - MenuItem( - id: 24, - description: "Filling for your taco.", - healthy: false, - name: "Corn & Beans" - ) - ]), - MenuCategory(category: "Desserts Dinner", items: [ - MenuItem( - id: 25, - description: "A chocolate chip cookie.", - healthy: false, - name: "Cookie" - ) - ]) - ]) - - static let rose = Eatery( - campusArea: "West", - events: [ - Event( - canonicalDay: Day().advanced(by: 0), - description: "Breakfast", - endTimestamp: Day().advanced(by: 0).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: roseBreakfastMenu, - startTimestamp: Day().advanced(by: 0).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 1), - description: "Lunch", - endTimestamp: Day().advanced(by: 1).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: roseLunchMenu, - startTimestamp: Day().advanced(by: 1).date(hour: 7, minute: 0).timeIntervalSince1970 - ), - Event( - canonicalDay: Day().advanced(by: 2), - description: "Dinner", - endTimestamp: Day().advanced(by: 2).date(hour: 24, minute: 0).timeIntervalSince1970, - menu: roseDinnerMenu, - startTimestamp: Day().advanced(by: 2).date(hour: 7, minute: 0).timeIntervalSince1970 - ) - ], - id: 1003, - imageUrl: nil, - latitude: nil, - locationDescription: "West Campus", - longitude: nil, - menuSummary: "Chef's choice", - name: "Rose Dining Hall", - onlineOrderUrl: nil, - paymentMethods: [.credit, .mealSwipes], - waitTimesByDay: [ - Day(): WaitTimes( - samples: (0 ..< 15).map { (index: Int) -> WaitTimeSample in - WaitTimeSample( - timestamp: Date().timeIntervalSince1970 + TimeInterval(index) * 60 * 15, - low: TimeInterval(index + 1) * 60, - expected: TimeInterval(index + 2) * 60, - high: TimeInterval(index + 3) * 60 - ) - }, - samplingMethod: .nearestNeighbor - ) - ] - ) -} diff --git a/Eatery Blue/UI/CompareMenus/CompareMenusEateryViewController.swift b/Eatery Blue/UI/CompareMenus/CompareMenusEateryViewController.swift index 44e03c98..9a2fd301 100644 --- a/Eatery Blue/UI/CompareMenus/CompareMenusEateryViewController.swift +++ b/Eatery Blue/UI/CompareMenus/CompareMenusEateryViewController.swift @@ -2,7 +2,7 @@ // CompareMenusEateryViewController.swift // Eatery Blue // -// Created by Peter Bidoshi on 3/5/24. +// Created by Peter Bidoshi on 3/5/24. // import EateryModel @@ -177,8 +177,8 @@ class CompareMenusEateryViewController: UIViewController { menuHasLoaded = true // Determine if this eatery has no retrievable menu - let hasMenu = (selectedEvent?.menu?.categories.isEmpty == false) - if !hasMenu { + let noMenu = selectedEvent?.menu.isEmpty ?? true + if noMenu { delegate?.compareMenusEateryViewControllerDidFailToLoadMenu(self) } } else { @@ -198,7 +198,7 @@ class CompareMenusEateryViewController: UIViewController { } private func setUpAnalytics(_ eatery: Eatery) { - if eatery.paymentMethods.contains(.mealSwipes) { + if eatery.paymentMethods.contains(.mealSwipe) { AppDevAnalytics.shared.logFirebase(CampusDiningCellPressPayload(diningHallName: eatery.name)) } else { AppDevAnalytics.shared.logFirebase(CampusCafeCellPressPayload(cafeName: eatery.name)) @@ -213,18 +213,14 @@ class CompareMenusEateryViewController: UIViewController { return } - if let menu = event.menu { - let sortedCategories = sortMenuCategories(categories: menu.categories) - if !sortedCategories.isEmpty { - for menuCategory in sortedCategories[..<(sortedCategories.count - 1)] { - addMenuCategory(menuCategory, isLast: false) - } + let sortedCategories = sortMenuCategories(categories: event.menu) + if !sortedCategories.isEmpty { + for menuCategory in sortedCategories[..<(sortedCategories.count - 1)] { + addMenuCategory(menuCategory, isLast: false) + } - if let last = sortedCategories.last { - addMenuCategory(last, isLast: true) - } - } else { - addSorryText() + if let last = sortedCategories.last { + addMenuCategory(last, isLast: true) } } else { addSorryText() @@ -310,13 +306,14 @@ class CompareMenusEateryViewController: UIViewController { var sortedCategories: [MenuCategory] = eatery?.name == "Morrison Dining" ? categories : categories.reversed() for i in 0 ..< sortedCategories.count { let menuCategory = sortedCategories[i] - if menuCategory.category == "Chef's Table" { + // Remove hardcoded categories and use a better sorting mechanism + if menuCategory.name == "Chef's Table" { sortedCategories.swapAt(0, i) } - if menuCategory.category == "Chef's Table - Sides" { + if menuCategory.name == "Chef's Table - Sides" { sortedCategories.swapAt(1, i) } - if menuCategory.category == "Grill" { + if menuCategory.name == "Grill" { sortedCategories.swapAt(2, i) } } @@ -328,7 +325,7 @@ class CompareMenusEateryViewController: UIViewController { categoryContainer.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) let categoryView = MenuCategoryView() - categoryView.titleLabel.text = menuCategory.category + categoryView.titleLabel.text = menuCategory.name categoryView.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) categoryView.backgroundColor = UIColor.Eatery.default00 @@ -357,7 +354,7 @@ class CompareMenusEateryViewController: UIViewController { stackView.addArrangedSubview(categoryContainer) categoryViews.append(categoryContainer) - menuCategoryPicker.addMenuCategory(categoryName: menuCategory.category) + menuCategoryPicker.addMenuCategory(categoryName: menuCategory.name) } private func scrollToCategoryView(at index: Int) { diff --git a/Eatery Blue/UI/EateryCard/EateryExpandableCardContentView.swift b/Eatery Blue/UI/EateryCard/EateryExpandableCardContentView.swift index 7e4be9fc..392a70e9 100644 --- a/Eatery Blue/UI/EateryCard/EateryExpandableCardContentView.swift +++ b/Eatery Blue/UI/EateryCard/EateryExpandableCardContentView.swift @@ -46,14 +46,14 @@ class EateryExpandableCardContentView: UIView { var event: Event? // Ignoring Late Lunch - if selectedMealType == "Breakfast" { - event = selectedEvents.first { $0.description == "Brunch" || $0.description == "Breakfast" } - } else if selectedMealType == "Lunch" { - event = selectedEvents.first { $0.description == "Brunch" || $0.description == "Lunch" } - } else if selectedMealType == "Dinner" { - event = selectedEvents.first { $0.description == "Dinner" } - } else if selectedMealType == "Late Dinner" { - event = selectedEvents.first { $0.description == "Late Night" } + if selectedMealType == .breakfast { + event = selectedEvents.first { $0.type == .brunch || $0.type == .breakfast } + } else if selectedMealType == .lunch { + event = selectedEvents.first { $0.type == .brunch || $0.type == .lunch } + } else if selectedMealType == .dinner { + event = selectedEvents.first { $0.type == .dinner } + } else if selectedMealType == .lateDinner { + event = selectedEvents.first { $0.type == .lateDinner } } if let event { @@ -66,7 +66,7 @@ class EateryExpandableCardContentView: UIView { eateryStatusLabel.text = EateryFormatter.default.formatEventTime(event) } - if event.endDate > Date() { + if event.endTimestamp > Date() { setupChevronArrow() } } diff --git a/Eatery Blue/UI/EateryCard/EateryExpandableCardDetailView.swift b/Eatery Blue/UI/EateryCard/EateryExpandableCardDetailView.swift index 0cae3486..fb9cbeab 100644 --- a/Eatery Blue/UI/EateryCard/EateryExpandableCardDetailView.swift +++ b/Eatery Blue/UI/EateryCard/EateryExpandableCardDetailView.swift @@ -33,27 +33,26 @@ class EateryExpandableCardDetailView: UIView { // MARK: - configure - func configure(eatery: Eatery, selectedDay: Day, selectedMealType: String, allEateries: [Eatery]) { + func configure(eatery: Eatery, selectedDay: Day, selectedMealType: EventType, allEateries: [Eatery]) { self.eatery = eatery self.allEateries = allEateries let selectedEvents = eatery.events.filter { $0.canonicalDay == selectedDay } - // MARK: Ideally this should be an enum but good for now - // Ignore late lunch var event: Event? - if selectedMealType == "Breakfast" { - event = selectedEvents.first { $0.description == "Brunch" || $0.description == "Breakfast" } - } else if selectedMealType == "Lunch" { - event = selectedEvents.first { $0.description == "Brunch" || $0.description == "Lunch" } - } else if selectedMealType == "Dinner" { - event = selectedEvents.first { $0.description == "Dinner" } - } else if selectedMealType == "Late Dinner" { - event = selectedEvents.first { $0.description == "Late Night" } + + if selectedMealType == .breakfast { + event = selectedEvents.first { $0.type == .brunch || $0.type == .breakfast } + } else if selectedMealType == .lunch { + event = selectedEvents.first { $0.type == .brunch || $0.type == .lunch } + } else if selectedMealType == .dinner { + event = selectedEvents.first { $0.type == .dinner } + } else if selectedMealType == .lateDinner { + event = selectedEvents.first { $0.type == .lateDinner } } - if let event, event.endDate > Date() { + if let event, event.endTimestamp > Date() { menuCategoryStackView.addArrangedSubview(HDivider()) addMenuCategories(event: event) setupViewEateryDetailsButton() @@ -101,27 +100,26 @@ class EateryExpandableCardDetailView: UIView { // MARK: - Helpers private func addMenuCategories(event: Event) { - if let categories = event.menu?.categories { - var sortedCategories: [MenuCategory] = eatery?.name == "Morrison Dining" ? categories : categories - .reversed() - for i in 0 ..< sortedCategories.count { - let menuCategory = sortedCategories[i] - if menuCategory.category == "Chef's Table" { - sortedCategories.swapAt(0, i) - } - if menuCategory.category == "Chef's Table - Sides" { - sortedCategories.swapAt(1, i) - } - if menuCategory.category == "Grill" { - sortedCategories.swapAt(2, i) - } + var sortedCategories: [MenuCategory] = eatery?.name == "Morrison Dining" ? event.menu : event.menu + .reversed() + for i in 0 ..< sortedCategories.count { + let menuCategory = sortedCategories[i] + // Remove hardcoded categories and use a better sorting mechanism + if menuCategory.name == "Chef's Table" { + sortedCategories.swapAt(0, i) } - - for category in sortedCategories { - let menuCategoryView = EateryExpandableCardMenuCategoryView() - menuCategoryView.configure(menuCategory: category) - menuCategoryStackView.addArrangedSubview(menuCategoryView) + if menuCategory.name == "Chef's Table - Sides" { + sortedCategories.swapAt(1, i) } + if menuCategory.name == "Grill" { + sortedCategories.swapAt(2, i) + } + } + + for category in sortedCategories { + let menuCategoryView = EateryExpandableCardMenuCategoryView() + menuCategoryView.configure(menuCategory: category) + menuCategoryStackView.addArrangedSubview(menuCategoryView) } } diff --git a/Eatery Blue/UI/EateryCard/EateryExpandableCardMenuCategoryView.swift b/Eatery Blue/UI/EateryCard/EateryExpandableCardMenuCategoryView.swift index fdcf4c88..1362ad39 100644 --- a/Eatery Blue/UI/EateryCard/EateryExpandableCardMenuCategoryView.swift +++ b/Eatery Blue/UI/EateryCard/EateryExpandableCardMenuCategoryView.swift @@ -48,7 +48,7 @@ class EateryExpandableCardMenuCategoryView: UIView { ) foodItemLabel.attributedText = attrString - categoryNameLabel.text = menuCategory.category + categoryNameLabel.text = menuCategory.name } // MARK: - Set Up Views diff --git a/Eatery Blue/UI/EateryCard/EateryLargeCardView.swift b/Eatery Blue/UI/EateryCard/EateryLargeCardView.swift index 439263ae..271ffddb 100644 --- a/Eatery Blue/UI/EateryCard/EateryLargeCardView.swift +++ b/Eatery Blue/UI/EateryCard/EateryLargeCardView.swift @@ -150,15 +150,15 @@ class EateryLargeCardView: UICollectionViewCell { } } - private func configureImageView(imageUrl: URL?, isOpen: Bool) { + private func configureImageView(imageUrl: String, isOpen: Bool) { imageView.image = UIImage() - imageView.kf.setImage(with: imageUrl) + imageView.kf.setImage(with: URL(string: imageUrl)) imageTintView.alpha = isOpen ? 0 : 0.5 - imageView.hero.id = imageUrl?.absoluteString + imageView.hero.id = imageUrl } private func configureSubtitleLabels(eatery: Eatery) { - subtitleLabels[0].text = eatery.locationDescription + subtitleLabels[0].text = eatery.location.validated() LocationManager.shared.$userLocation .sink { userLocation in self.subtitleLabels[1].attributedText = EateryFormatter.default.formatEatery( @@ -182,13 +182,13 @@ class EateryLargeCardView: UICollectionViewCell { switch status { case let .closingSoon(event): let alert = EateryCardAlertView() - let minutesUntilClosed = Int(round(event.endDate.timeIntervalSince(now) / 60)) + let minutesUntilClosed = Int(round(event.endTimestamp.timeIntervalSince(now) / 60)) alert.titleLabel.text = "Closing in \(minutesUntilClosed) min" addAlertView(alert) case let .openingSoon(event): let alert = EateryCardAlertView() - let minutesUntilOpen = Int(round(event.startDate.timeIntervalSince(now) / 60)) + let minutesUntilOpen = Int(round(event.startTimestamp.timeIntervalSince(now) / 60)) alert.titleLabel.text = "Opening in \(minutesUntilOpen) min" addAlertView(alert) diff --git a/Eatery Blue/UI/EateryCard/EateryMediumCardView.swift b/Eatery Blue/UI/EateryCard/EateryMediumCardView.swift index 68afcf03..c04c5598 100644 --- a/Eatery Blue/UI/EateryCard/EateryMediumCardView.swift +++ b/Eatery Blue/UI/EateryCard/EateryMediumCardView.swift @@ -176,12 +176,15 @@ class EateryMediumCardView: UICollectionViewCell { } } - private func configureImageView(imageUrl: URL?, isOpen: Bool) { - imageView.kf.setImage( - with: imageUrl, - options: [.backgroundDecode] - ) - imageTintView.alpha = isOpen ? 0 : 0.5 + private func configureImageView(imageUrl: String, isOpen: Bool) { + if let url = URL(string: imageUrl) { + imageView.kf.setImage( + with: url, + options: [.backgroundDecode] + ) + + imageTintView.alpha = isOpen ? 0 : 0.5 + } } private func configureSubtitleLabels(eatery: Eatery) { @@ -202,12 +205,12 @@ class EateryMediumCardView: UICollectionViewCell { let now = Date() switch status { case let .closingSoon(event): - let minutesUntilClosed = Int(round(event.endDate.timeIntervalSince(now) / 60)) + let minutesUntilClosed = Int(round(event.startTimestamp.timeIntervalSince(now) / 60)) alertView.titleLabel.text = "Closing in \(minutesUntilClosed) min" alertView.isHidden = false case let .openingSoon(event): - let minutesUntilOpen = Int(round(event.startDate.timeIntervalSince(now) / 60)) + let minutesUntilOpen = Int(round(event.startTimestamp.timeIntervalSince(now) / 60)) alertView.titleLabel.text = "Opening in \(minutesUntilOpen) min" alertView.isHidden = false diff --git a/Eatery Blue/UI/EateryCard/EaterySmallCardView.swift b/Eatery Blue/UI/EateryCard/EaterySmallCardView.swift index 934f7da9..c8e8592a 100644 --- a/Eatery Blue/UI/EateryCard/EaterySmallCardView.swift +++ b/Eatery Blue/UI/EateryCard/EaterySmallCardView.swift @@ -127,10 +127,10 @@ class EaterySmallCardView: UICollectionViewCell { } private func configureImageView() { - guard let eatery else { return } + guard let eatery, let imageUrl = URL(string: eatery.imageUrl) else { return } - imageView.kf.setImage(with: eatery.imageUrl) - imageView.hero.id = eatery.imageUrl?.absoluteString + imageView.kf.setImage(with: imageUrl) + imageView.hero.id = eatery.imageUrl imageView.alpha = eatery.isOpen ? 1 : 0.5 } diff --git a/Eatery Blue/UI/EateryFilter/EateryFilterViewController.swift b/Eatery Blue/UI/EateryFilter/EateryFilterViewController.swift index 32330487..0c1506d7 100644 --- a/Eatery Blue/UI/EateryFilter/EateryFilterViewController.swift +++ b/Eatery Blue/UI/EateryFilter/EateryFilterViewController.swift @@ -229,7 +229,7 @@ extension EateryFilterViewController: PaymentMethodsFilterSheetViewControllerDel for paymentMethod in filter.paymentMethods { switch paymentMethod { case .brbs: AppDevAnalytics.shared.logFirebase(BRBFilterPressPayload()) - case .mealSwipes: AppDevAnalytics.shared.logFirebase(SwipesFilterPressPayload()) + case .mealSwipe: AppDevAnalytics.shared.logFirebase(SwipesFilterPressPayload()) default: break } } diff --git a/Eatery Blue/UI/EateryScreen/EateryModelController.swift b/Eatery Blue/UI/EateryScreen/EateryModelController.swift index e12c4618..89d5f0fb 100644 --- a/Eatery Blue/UI/EateryScreen/EateryModelController.swift +++ b/Eatery Blue/UI/EateryScreen/EateryModelController.swift @@ -120,7 +120,7 @@ class EateryModelController: EateryViewController { } private func setUpStackView(_ eatery: Eatery) { - addHeaderImageView(imageUrl: eatery.imageUrl) + addHeaderImageView(url: eatery.imageUrl) addPaymentMethodsView(headerView: stackView.arrangedSubviews.last, paymentMethods: eatery.paymentMethods) addPlaceDecorationIcon(headerView: stackView.arrangedSubviews.last) addNameLabel(eatery.name) @@ -130,17 +130,12 @@ class EateryModelController: EateryViewController { addButtons(eatery) addAlertsIfNeeded(eatery) addTimingView(eatery) - - // if this is the free food fridge add the description - if eatery.id == 46 { - addFreedgeDescription() - } - + addAboutDescription(eatery) addSpacer(height: 16) } func setUpAnalytics(_ eatery: Eatery) { - if eatery.paymentMethods.contains(.mealSwipes) { + if eatery.paymentMethods.contains(.mealSwipe) { AppDevAnalytics.shared.logFirebase(CampusDiningCellPressPayload(diningHallName: eatery.name)) } else { AppDevAnalytics.shared.logFirebase(CampusCafeCellPressPayload(cafeName: eatery.name)) @@ -179,20 +174,15 @@ class EateryModelController: EateryViewController { private func addButtons(_ eatery: Eatery) { addButtons( orderOnlineAction: eatery.onlineOrderUrl != nil ? didPressOrderOnlineButton : nil, - directionsAction: eatery.latitude != nil && eatery.longitude != nil ? didPressDirectionsButton : nil + directionsAction: didPressDirectionsButton ) } private func addAlertsIfNeeded(_ eatery: Eatery) { let now = Date() - let relevantAlerts = eatery.alerts.filter { alert in - alert.startDate <= now && now <= alert.endDate - } - for alert in relevantAlerts { - if let description = alert.description { - addAlertInfoView(description) - } + for alert in eatery.announcements { + addAlertInfoView(alert) } } @@ -210,17 +200,17 @@ class EateryModelController: EateryViewController { } private func updateNavigationViewCategoriesFromState() { - guard let event = selectedEvent, let menu = event.menu else { + guard let event = selectedEvent, !event.menu.isEmpty else { navigationView.scrollView.isHidden = true return } navigationView.removeAllCategories() - let sortedCategories = sortMenuCategories(categories: menu.categories) + let sortedCategories = sortMenuCategories(categories: event.menu) navigationView.scrollView.isHidden = sortedCategories.isEmpty for (i, menuCategory) in sortedCategories.enumerated() { - navigationView.addCategory(menuCategory.category) { [self] in + navigationView.addCategory(menuCategory.name) { [self] in scrollToCategoryView(at: i) } } @@ -262,7 +252,7 @@ class EateryModelController: EateryViewController { presentMenuPicker() } addSpacer(height: 16) - addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { Int64($0.id) }) + addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { $0.id }) addViewProportionalSpacer(multiplier: 0.5) return } @@ -282,9 +272,9 @@ class EateryModelController: EateryViewController { let title: String if event.canonicalDay == Day() { - title = event.description ?? "Full Menu" + title = event.type.description } else { - title = "\(weekdayFormatter.string(from: event.canonicalDay.date())) \(event.description ?? "Menu")" + title = "\(weekdayFormatter.string(from: event.canonicalDay.date())) \(event.type.description)" } addMenuHeaderView( @@ -300,8 +290,7 @@ class EateryModelController: EateryViewController { let events = eatery?.events .filter { - $0.canonicalDay == selectedEvent?.canonicalDay && $0 - .menu != nil && !($0.menu?.categories.isEmpty ?? true) + $0.canonicalDay == selectedEvent?.canonicalDay && !($0.menu.isEmpty) } ?? [] if events.count <= 1 { addSpacer(height: 16) @@ -319,29 +308,25 @@ class EateryModelController: EateryViewController { // Treat categories with no items as absent // If all categories are empty, show the inline error state - if let menu = event.menu { - // Filter out categories whose items are empty - let nonEmptyCategories = menu.categories.filter { !$0.items.isEmpty } + // Filter out categories whose items are empty + let nonEmptyCategories = event.menu.filter { !$0.items.isEmpty } - if nonEmptyCategories.isEmpty { - addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { Int64($0.id) }) - } else { - let sortedCategories = sortMenuCategories(categories: nonEmptyCategories) - if !sortedCategories.isEmpty { - for menuCategory in sortedCategories[..<(sortedCategories.count - 1)] { - addMenuCategory(menuCategory) - addSpacer(height: 8) - } + if nonEmptyCategories.isEmpty { + addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { $0.id }) + } else { + let sortedCategories = sortMenuCategories(categories: nonEmptyCategories) + if !sortedCategories.isEmpty { + for menuCategory in sortedCategories[..<(sortedCategories.count - 1)] { + addMenuCategory(menuCategory) + addSpacer(height: 8) + } - if let last = sortedCategories.last { - addMenuCategory(last) - } - } else { - addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { Int64($0.id) }) + if let last = sortedCategories.last { + addMenuCategory(last) } + } else { + addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { $0.id }) } - } else { - addInlineErrorBlock(reportIssueEateryId: eatery.flatMap { Int64($0.id) }) } } @@ -349,13 +334,16 @@ class EateryModelController: EateryViewController { var sortedCategories: [MenuCategory] = eatery?.name == "Morrison Dining" ? categories : categories.reversed() for i in 0 ..< sortedCategories.count { let menuCategory = sortedCategories[i] - if menuCategory.category == "Chef's Table" { + // Remove hardcoded categories and use a better sorting mechanism + if menuCategory.name == "Chef's Table" { sortedCategories.swapAt(0, i) } - if menuCategory.category == "Chef's Table - Sides" { + + if menuCategory.name == "Chef's Table - Sides" { sortedCategories.swapAt(1, i) } - if menuCategory.category == "Grill" { + + if menuCategory.name == "Grill" { sortedCategories.swapAt(2, i) } } @@ -371,7 +359,7 @@ class EateryModelController: EateryViewController { if let eatery = eatery { for event in eatery.events { menuChoices.append(MenuPickerSheetViewController.MenuChoice( - description: event.description ?? "Event", + description: event.type.description, event: event )) } @@ -387,17 +375,17 @@ class EateryModelController: EateryViewController { } private func didPressOrderOnlineButton() { - if let url = eatery?.onlineOrderUrl { + if let stringUrl = eatery?.onlineOrderUrl, let url = URL(string: stringUrl) { UIApplication.shared.open(url, options: [:]) } } private func didPressDirectionsButton() { - guard let eatery = eatery, let latitude = eatery.latitude, let longitude = eatery.longitude else { + guard let eatery = eatery else { return } - let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + let coordinate = CLLocationCoordinate2D(latitude: Double(eatery.latitude), longitude: Double(eatery.longitude)) let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: coordinate)) mapItem.name = eatery.name mapItem.openInMaps(launchOptions: [ @@ -405,7 +393,7 @@ class EateryModelController: EateryViewController { ]) } - private func addInlineErrorBlock(reportIssueEateryId: Int64?) { + private func addInlineErrorBlock(reportIssueEateryId: Int?) { let container = UIView() container.isUserInteractionEnabled = false diff --git a/Eatery Blue/UI/EateryScreen/EateryViewController.swift b/Eatery Blue/UI/EateryScreen/EateryViewController.swift index 601ddc46..f04d6a7f 100644 --- a/Eatery Blue/UI/EateryScreen/EateryViewController.swift +++ b/Eatery Blue/UI/EateryScreen/EateryViewController.swift @@ -135,7 +135,8 @@ class EateryViewController: UIViewController { spinner.removeFromSuperview() } - func addHeaderImageView(imageUrl: URL?) { + func addHeaderImageView(url: String) { + let imageUrl = URL(string: url) let imageView = UIImageView() imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill @@ -152,7 +153,7 @@ class EateryViewController: UIViewController { headerView = imageView } - func addPaymentMethodsView(headerView: UIView?, paymentMethods: Set) { + func addPaymentMethodsView(headerView: UIView?, paymentMethods: [PaymentMethod]) { guard let headerView = headerView, !paymentMethods.isEmpty else { return } @@ -163,7 +164,7 @@ class EateryViewController: UIViewController { stack.distribution = .fill stack.alignment = .fill - if paymentMethods.contains(.mealSwipes) { + if paymentMethods.contains(.mealSwipe) { let imageView = UIImageView() imageView.image = UIImage(named: "MealSwipes")?.withRenderingMode(.alwaysTemplate) imageView.tintColor = UIColor.Eatery.blue @@ -189,7 +190,7 @@ class EateryViewController: UIViewController { stack.addArrangedSubview(imageView) } - if paymentMethods.contains(.cash) || paymentMethods.contains(.credit) { + if paymentMethods.contains(.cash) || paymentMethods.contains(.card) { let imageView = UIImageView() imageView.image = UIImage(named: "Cash")?.withRenderingMode(.alwaysTemplate) imageView.tintColor = UIColor.Eatery.green @@ -223,7 +224,7 @@ class EateryViewController: UIViewController { container.tap { [self] _ in let viewController = PaymentMethodsSheetViewController() viewController.setUpSheetPresentation() - viewController.setPaymentMethods(paymentMethods) + viewController.setPaymentMethods(Set(paymentMethods)) tabBarController?.present(viewController, animated: true) } } @@ -264,10 +265,13 @@ class EateryViewController: UIViewController { } func addShortDescriptionLabel(_ eatery: Eatery) { + let eateryTypes = eatery.eateryTypes.validated()?.formatted() + let location = eatery.location.validated() + let label = UILabel() label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 - label.text = "\(eatery.locationDescription ?? "--") · \(eatery.menuSummary ?? "--")" + label.text = "\(location ?? "--") · \(eateryTypes ?? "--")" label.textColor = UIColor.Eatery.secondaryText label.font = .preferredFont(for: .subheadline, weight: .semibold) @@ -373,57 +377,69 @@ class EateryViewController: UIViewController { return cell } - func addFreedgeDescription() { + func addAboutDescription(_ eatery: Eatery) { let label = UILabel() label.numberOfLines = 0 - label.text = """ - The Free Food Fridge is a place on campus where all are welcome to both donate and take food at no cost, \ - as needed, reducing food waste while improving food access. The fridge is intended to provide a \n - convenient place to drop off food that might otherwise be discarded––it should not be used to redirect \ - food that is already being donated. - """ + if let shortAbout = eatery.shortAbout.validated() { + label.text = shortAbout + } else if eatery.id == 46 { + label.text = """ + The Free Food Fridge is a place on campus where all are welcome to both donate and take food at no cost, \ + as needed, reducing food waste while improving food access. The fridge is intended to provide a \n + convenient place to drop off food that might otherwise be discarded––it should not be used to redirect \ + food that is already being donated. + """ + } else { + label.text = nil + } + label.textColor = UIColor.Eatery.secondaryText label.font = .preferredFont(for: .footnote, weight: .regular) let container = ContainerView(content: label) container.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - stackView.addArrangedSubview(container) - } - - private func createWaitTimeCell(_ eatery: Eatery) -> TimingCellView { - let cell = TimingCellView() - cell.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) - - cell.titleLabel.textColor = UIColor.Eatery.secondaryText - let text = NSMutableAttributedString() - text.append(NSAttributedString( - attachment: NSTextAttachment(image: UIImage(named: "Watch"), scaledToMatch: cell.titleLabel.font) - )) - text.append(NSAttributedString(string: " Wait Time")) - cell.titleLabel.attributedText = text - - if let waitTimes = eatery.waitTimesByDay[Day()], let sample = waitTimes.sample(at: Date()) { - cell.statusLabel.textColor = UIColor.Eatery.primaryText - let low = Int(round(sample.low / 60)) - let high = Int(round(sample.high / 60)) - cell.statusLabel.text = low < high ? "\(low)-\(high) minutes" : "\(low) minutes" - - let events = eatery.events - cell.tap { [self] _ in - let viewController = WaitTimesSheetViewController() - viewController.setUpSheetPresentation() - viewController.setUp(eatery.id, waitTimes, events: events) - tabBarController?.present(viewController, animated: true) - } - } else { - cell.statusLabel.textColor = UIColor.Eatery.secondaryText - cell.statusLabel.text = "-- minutes" + if label.text != nil { + stackView.addArrangedSubview(container) } - - return cell } + // Wait times not supported as of 1/29/2026 + +// private func createWaitTimeCell(_ eatery: Eatery) -> TimingCellView { +// let cell = TimingCellView() +// cell.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) +// +// cell.titleLabel.textColor = UIColor.Eatery.secondaryText +// let text = NSMutableAttributedString() +// text.append(NSAttributedString( +// attachment: NSTextAttachment(image: UIImage(named: "Watch"), scaledToMatch: cell.titleLabel.font) +// )) +// text.append(NSAttributedString(string: " Wait Time")) +// cell.titleLabel.attributedText = text +// +// if let waitTimes = eatery.waitTimesByDay[Day()], let sample = waitTimes.sample(at: Date()) { +// cell.statusLabel.textColor = UIColor.Eatery.primaryText +// let low = Int(round(sample.low / 60)) +// let high = Int(round(sample.high / 60)) +// cell.statusLabel.text = low < high ? "\(low)-\(high) minutes" : "\(low) minutes" +// +// let events = eatery.events +// cell.tap { [self] _ in +// let viewController = WaitTimesSheetViewController() +// viewController.setUpSheetPresentation() +// viewController.setUp(eatery.id, waitTimes, events: events) +// tabBarController?.present(viewController, animated: true) +// } +// +// } else { +// cell.statusLabel.textColor = UIColor.Eatery.secondaryText +// cell.statusLabel.text = "-- minutes" +// } +// +// return cell +// } + func addSpacer(height: CGFloat, color: UIColor? = UIColor.Eatery.gray00) { let spacer = UIView() spacer.backgroundColor = color @@ -443,7 +459,7 @@ class EateryViewController: UIViewController { } func addMenuHeaderView( - eateryId: Int64?, + eateryId: Int?, title: String, subtitle: String, dropDownButtonAction: (() -> Void)? = nil @@ -475,13 +491,12 @@ class EateryViewController: UIViewController { for i in 0 ..< eatery.events.count { let event = eatery.events[i] if event.canonicalDay != selectedEvent.canonicalDay { continue } - guard let menu = event.menu else { continue } - if menu.categories.isEmpty { continue } + if event.menu.isEmpty { continue } let isSelectedEvent = eatery.events[i] == selectedEvent let button = UIButton() - button.setTitle(event.description, for: .normal) + button.setTitle(event.type.description, for: .normal) button.setTitleColor(isSelectedEvent ? .Eatery.primaryText : .Eatery.gray01, for: .normal) button.titleLabel?.font = .preferredFont(for: .body, weight: isSelectedEvent ? .semibold : .medium) @@ -529,7 +544,7 @@ class EateryViewController: UIViewController { func addMenuCategory(_ menuCategory: MenuCategory) { let categoryView = MenuCategoryView() - categoryView.titleLabel.text = menuCategory.category + categoryView.titleLabel.text = menuCategory.name categoryView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) for item in menuCategory.items { @@ -542,7 +557,7 @@ class EateryViewController: UIViewController { stackView.addArrangedSubview(categoryView) } - func addReportIssueView(eateryId: Int64? = nil) { + func addReportIssueView(eateryId: Int? = nil) { let view = ReportIssueView() view.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) view.button.tap { [self] _ in diff --git a/Eatery Blue/UI/EateryScreen/MenuItemView.swift b/Eatery Blue/UI/EateryScreen/MenuItemView.swift index 0bb3ab66..26540a88 100644 --- a/Eatery Blue/UI/EateryScreen/MenuItemView.swift +++ b/Eatery Blue/UI/EateryScreen/MenuItemView.swift @@ -12,7 +12,7 @@ class MenuItemView: UIView { // MARK: - Properties (view) private let allergensLabel = UILabel() - private let descriptionLabel = UILabel() +// private let descriptionLabel = UILabel() private let dietaryInfoStack = UIStackView() private let favoriteButton = ButtonView(content: UIView()) private let favoriteButtonImage = UIImageView() @@ -27,10 +27,10 @@ class MenuItemView: UIView { didSet { setUpTitleLabel() setUpPriceLabel() - setUpDescriptionLabel() +// setUpDescriptionLabel() setUpFavoriteButton() - setUpAllergensLabel(item?.allergens ?? []) - setUpDietaryInfoStack(item?.dietaryPreferences ?? []) + setUpAllergensLabel(item?.allergens) + setUpDietaryInfoStack(item?.dietaryPreferences) } } @@ -82,8 +82,8 @@ class MenuItemView: UIView { favoriteButton.addSubview(favoriteButtonImage) setUpFavoriteButton() - stackView.addArrangedSubview(descriptionLabel) - setUpDescriptionLabel() +// stackView.addArrangedSubview(descriptionLabel) +// setUpDescriptionLabel() stackView.addArrangedSubview(allergensLabel) setUpAllergensLabel([]) @@ -96,7 +96,7 @@ class MenuItemView: UIView { } private func setUpPriceLabel() { - if let price = item?.price { + if let price = item?.basePrice { priceLabel.text = EateryViewController .priceNumberFormatter .string(from: NSNumber(value: Double(price) / 100)) @@ -108,19 +108,21 @@ class MenuItemView: UIView { priceLabel.textColor = UIColor.Eatery.secondaryText } - private func setUpDescriptionLabel() { - if let description = item?.description { - descriptionLabel.isHidden = false - descriptionLabel.text = description - } else { - descriptionLabel.isHidden = true - } + // As of refactored backend, there is no description for items yet (1/29/2026) - descriptionLabel.font = .preferredFont(for: .footnote, weight: .regular) - descriptionLabel.textColor = UIColor.Eatery.secondaryText - descriptionLabel.isHidden = true - descriptionLabel.numberOfLines = 0 - } +// private func setUpDescriptionLabel() { +// if let description = item?.description { +// descriptionLabel.isHidden = false +// descriptionLabel.text = description +// } else { +// descriptionLabel.isHidden = true +// } +// +// descriptionLabel.font = .preferredFont(for: .footnote, weight: .regular) +// descriptionLabel.textColor = UIColor.Eatery.secondaryText +// descriptionLabel.isHidden = true +// descriptionLabel.numberOfLines = 0 +// } private func setUpFavoriteButton() { guard let item = item else { return } @@ -142,30 +144,28 @@ class MenuItemView: UIView { } } - private func setUpAllergensLabel(_ allergens: [String]) { - if allergens.isEmpty { + private func setUpAllergensLabel(_ allergens: [Allergen]?) { + guard let allergens, !allergens.isEmpty else { allergensLabel.isHidden = true return - } else { - allergensLabel.isHidden = false } - allergensLabel.text = "Contains: \(allergens.joined(separator: ", "))" + allergensLabel.isHidden = false + allergensLabel.text = "Contains: \(allergens.map { $0.name }.joined(separator: ", "))" allergensLabel.font = .systemFont(ofSize: 10) allergensLabel.textColor = UIColor.Eatery.red } - private func setUpDietaryInfoStack(_ dietaryInfos: [String]) { - dietaryInfoStack.axis = .horizontal - dietaryInfoStack.spacing = 8 - - if dietaryInfos.isEmpty { + private func setUpDietaryInfoStack(_ dietaryInfos: [DietaryPreference]?) { + guard let dietaryInfos, !dietaryInfos.isEmpty else { dietaryInfoStack.isHidden = true return - } else { - dietaryInfoStack.isHidden = false } + dietaryInfoStack.isHidden = false + dietaryInfoStack.axis = .horizontal + dietaryInfoStack.spacing = 8 + for view in dietaryInfoStack.subviews { dietaryInfoStack.removeArrangedSubview(view) view.removeFromSuperview() @@ -174,7 +174,7 @@ class MenuItemView: UIView { for dietaryInfo in dietaryInfos { let dietaryInfoView = UIImageView() // see if the dietaryInfo image exists - if let image = UIImage(named: dietaryInfo) { + if let image = UIImage(named: dietaryInfo.name) { dietaryInfoView.image = image dietaryInfoStack.addArrangedSubview(dietaryInfoView) dietaryInfoView.snp.makeConstraints { make in diff --git a/Eatery Blue/UI/Favoriting/FavoritesItemsTableViewCell.swift b/Eatery Blue/UI/Favoriting/FavoritesItemsTableViewCell.swift index dc2cc9e5..0681c53b 100644 --- a/Eatery Blue/UI/Favoriting/FavoritesItemsTableViewCell.swift +++ b/Eatery Blue/UI/Favoriting/FavoritesItemsTableViewCell.swift @@ -40,7 +40,7 @@ class FavoritesItemsTableViewCell: UITableViewCell { // MARK: - Configure - func configure(item: ItemMetadata, expanded: Bool, itemData: [String: Set]?) { + func configure(item: ItemMetadata, expanded: Bool, itemData: [EventType: Set]?) { itemNameLabel.text = item.itemName availableLabel.text = itemData != nil ? "Available Today" : "Not Available" availableLabel.textColor = itemData != nil ? .Eatery.green : .Eatery.gray03 @@ -61,18 +61,21 @@ class FavoritesItemsTableViewCell: UITableViewCell { } // these categories should appear first if they are available - let firstKeys = ["Breakfast", "Brunch", "Lunch", "Dinner"] + let firstKeys: [EventType] = [.breakfast, .brunch, .lunch, .lateLunch, .dinner, .lateDinner] - for key in firstKeys { - if let eateries = itemData[key] { - addCategory(key, eateries: eateries.sorted()) + for category in firstKeys { + if let eateries = itemData[category] { + addCategory(category.description, eateries: eateries.sorted()) } } + // then the rest for category in itemData.keys { if firstKeys.contains(category) { continue } - addCategory(category, eateries: itemData[category]?.sorted() ?? []) + if let eateries = itemData[category] { + addCategory(category.description, eateries: eateries.sorted()) + } } } diff --git a/Eatery Blue/UI/Favoriting/FavoritesItemsView.swift b/Eatery Blue/UI/Favoriting/FavoritesItemsView.swift index 08255c4f..8345b55b 100644 --- a/Eatery Blue/UI/Favoriting/FavoritesItemsView.swift +++ b/Eatery Blue/UI/Favoriting/FavoritesItemsView.swift @@ -30,7 +30,7 @@ class FavoritesItemsView: UIView { } private var expanded: [Int] = [] - private var itemData: [String: [String: Set]] = [:] + private var itemData: [String: [EventType: Set]] = [:] private var sortedFavorites: [ItemMetadata] = [] private lazy var dataSource = makeDataSource() @@ -75,7 +75,7 @@ class FavoritesItemsView: UIView { private func findFavorites() { let favoriteIdsSet: Set = Set(favoriteItems.compactMap { $0.itemName }) - var favoriteItemsData = [(eateryName: String, timeOfDay: String, itemName: String)]() + var favoriteItemsData = [(eateryName: String, type: EventType, itemName: String)]() let today = Day() @@ -84,30 +84,29 @@ class FavoritesItemsView: UIView { for event in eatery.events { if event.canonicalDay != today { continue } - guard let timeOfDay = event.description else { continue } - let favoriteItemsInEvent = event.menu?.categories.flatMap { category in + let favoriteItemsInEvent = event.menu.flatMap { category in category.items.filter { favoriteIdsSet.contains($0.name) }.map { item in - (eateryName: eateryName, timeOfDay: timeOfDay, itemName: item.name) + (eateryName: eateryName, type: event.type, itemName: item.name) } - } ?? [] + } favoriteItemsData.append(contentsOf: favoriteItemsInEvent) } } for data in favoriteItemsData { - let (eateryName, timeOfDay, itemName) = data + let (eateryName, type, itemName) = data if itemData[itemName] == nil { itemData[itemName] = [:] } - if itemData[itemName]![timeOfDay] == nil { - itemData[itemName]![timeOfDay] = [] + if itemData[itemName]![type] == nil { + itemData[itemName]![type] = [] } - itemData[itemName]![timeOfDay]!.insert(eateryName) + itemData[itemName]![type]!.insert(eateryName) } sortedFavorites = favoriteItems.sorted { itemData[$1.itemName ?? ""] == nil } diff --git a/Eatery Blue/UI/HomeScreen/HomeViewController.swift b/Eatery Blue/UI/HomeScreen/HomeViewController.swift index f7c8f2ec..02886677 100644 --- a/Eatery Blue/UI/HomeScreen/HomeViewController.swift +++ b/Eatery Blue/UI/HomeScreen/HomeViewController.swift @@ -98,7 +98,7 @@ class HomeViewController: UIViewController { Task { do { startLoading() - try await updateSimpleEateriesFromNetworking() + try await updateAllEateriesFromNetworking() stopLoading() trySetUpCompareMenusOnboarding() } catch { @@ -108,11 +108,6 @@ class HomeViewController: UIViewController { applySnapshot(animated: true) } } - - // For caching purposes. Although we might not need it now we will likely need it in a bit - Task { - await updateAllEateriesFromNetworking() - } } override func viewSafeAreaInsetsDidChange() { @@ -262,7 +257,7 @@ class HomeViewController: UIViewController { guard let self else { return } do { - try await updateSimpleEateriesFromNetworking() + try await updateAllEateriesFromNetworking() } catch { logger.error("\(#function): \(error)") } @@ -280,23 +275,9 @@ class HomeViewController: UIViewController { } } - /// Start loading all eateries in the background, will be cached if needs to be refetched - private func updateAllEateriesFromNetworking() async { - do { - _ = Constants.isTesting ? DummyData.eateries : try await Networking.default.loadAllEatery() - } catch { - logger.error("\(error)") - } - } - - /// Request the simple eateries from the networking layer. - private func updateSimpleEateriesFromNetworking() async throws { - let eateries = Constants.isTesting ? DummyData.eateries : try await Networking.default.loadSimpleEateries() - allEateries = eateries.filter { eatery in - !eatery.name.isEmpty - }.sorted(by: { - $0.isOpen == $1.isOpen ? $0.name < $1.name : $0.isOpen - }) + /// Load all eateries from the networking layer. + private func updateAllEateriesFromNetworking() async throws { + allEateries = Constants.isTesting ? Eatery.dummyEateries : try await Networking.default.loadAllEatery() HomeViewController.cachedAllEateries = allEateries } diff --git a/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentModelController.swift b/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentModelController.swift index 36d194e1..d799674e 100644 --- a/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentModelController.swift +++ b/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentModelController.swift @@ -27,16 +27,24 @@ class HomeSearchContentModelController: HomeSearchContentViewController { return [ FuseProperty(name: eatery.name, weight: 1), FuseProperty( - name: eatery.locationDescription ?? "", - weight: eatery.locationDescription != nil ? 0.25 : 0 + name: eatery.location, + weight: eatery.location.validated() != nil ? 0.25 : 0 ) ] case let .menuItem(item, category, _): return [ FuseProperty(name: item.name, weight: 1), - FuseProperty(name: item.description ?? "", weight: item.description != nil ? 0.5 : 0), - FuseProperty(name: category, weight: 0.5) +// FuseProperty(name: item.description ?? "", weight: item.description != nil ? 0.5 : 0), + FuseProperty(name: category, weight: 0.5), + FuseProperty( + name: item.allergens.map { $0.name }.joined(separator: ", "), + weight: !item.allergens.isEmpty ? 0.25 : 0 + ), + FuseProperty( + name: item.dietaryPreferences.map { $0.name }.joined(separator: ", "), + weight: !item.dietaryPreferences.isEmpty ? 0.25 : 0 + ) ] } } @@ -122,17 +130,13 @@ class HomeSearchContentModelController: HomeSearchContentViewController { searchItems.append(.eatery(eatery)) let eventsTodayAfterNow = eatery.events.filter { event in - event.canonicalDay == today && now <= event.endDate + event.canonicalDay == today && now <= event.endTimestamp } for event in eventsTodayAfterNow { - guard let menu = event.menu else { - continue - } - - for category in menu.categories { + for category in event.menu { for item in category.items { - searchItems.append(.menuItem(item, category: category.category, eatery)) + searchItems.append(.menuItem(item, category: category.name, eatery)) } } } @@ -211,7 +215,7 @@ class HomeSearchContentModelController: HomeSearchContentViewController { } } - private func addRecentSearch(eateryID: Int64?, type: String, title: String, subtitle: String?) { + private func addRecentSearch(eateryID: Int?, type: String, title: String, subtitle: String?) { let coreDataStack = AppDelegate.shared.coreDataStack let context = coreDataStack.context @@ -234,7 +238,7 @@ class HomeSearchContentModelController: HomeSearchContentViewController { let recentSearch = RecentSearch(context: context) recentSearch.dateAdded = Date() - recentSearch.eateryID = eateryID ?? 0 + recentSearch.eateryID = Int64(eateryID ?? 0) recentSearch.type = type recentSearch.title = title recentSearch.subtitle = subtitle diff --git a/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentViewController.swift b/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentViewController.swift index 3f478f26..1bf0fb2c 100644 --- a/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentViewController.swift +++ b/Eatery Blue/UI/HomeSearchScreen/HomeSearchContentViewController.swift @@ -151,22 +151,24 @@ extension HomeSearchContentViewController: UITableViewDataSource { view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) view.titleLabel.text = item.name - if let price = item.price { + if let price = item.basePrice { view.priceLabel.text = priceNumberFormatter .string(from: NSNumber(value: Double(price) / 100)) } else { view.priceLabel.text = "" } - if let description = item.description { - view.descriptionLabel.isHidden = false - view.descriptionLabel.text = description - } else { - view.descriptionLabel.isHidden = true - } + // Item doesn't have a description as of backend refactor 01/29/2026 + +// if let description = item.description { +// view.descriptionLabel.isHidden = false +// view.descriptionLabel.text = description +// } else { +// view.descriptionLabel.isHidden = true +// } if let eatery = eatery { - if let locationDescription = eatery.locationDescription { + if let locationDescription = eatery.location.validated() { view.sourceLabel.text = "\(eatery.name) · \(locationDescription)" } else { view.sourceLabel.text = eatery.name diff --git a/Eatery Blue/UI/HoursSheet/HoursSheetViewController.swift b/Eatery Blue/UI/HoursSheet/HoursSheetViewController.swift index 6be6a4d6..70af4cbb 100644 --- a/Eatery Blue/UI/HoursSheet/HoursSheetViewController.swift +++ b/Eatery Blue/UI/HoursSheet/HoursSheetViewController.swift @@ -16,7 +16,7 @@ class HoursSheetViewController: SheetViewController { return formatter }() - func setUp(_ eateryId: Int64, _ events: [Event]) { + func setUp(_ eateryId: Int, _ events: [Event]) { addHeader(title: "Hours", image: UIImage(named: "Clock")) addStatusLabel(EateryFormatter.default.formatStatus(EateryStatus(events))) addSchedule(events) @@ -39,7 +39,7 @@ class HoursSheetViewController: SheetViewController { dayToDescription[day] = EateryFormatter.default.formatEventTimes(events.filter { $0.canonicalDay == day }.sorted(by: { lhs, rhs in - lhs.startDate < rhs.startDate + lhs.startTimestamp < rhs.startTimestamp })) } diff --git a/Eatery Blue/UI/MenuPickerSheet/MenuPickerSheetViewController.swift b/Eatery Blue/UI/MenuPickerSheet/MenuPickerSheetViewController.swift index facfb4bf..3a0d6355 100644 --- a/Eatery Blue/UI/MenuPickerSheet/MenuPickerSheetViewController.swift +++ b/Eatery Blue/UI/MenuPickerSheet/MenuPickerSheetViewController.swift @@ -150,7 +150,7 @@ class MenuPickerSheetViewController: SheetViewController { private func filterMenuChoices(on day: Day) -> [MenuChoice] { menuChoices.filter { - $0.event.canonicalDay == day && $0.event.menu != nil && !($0.event.menu?.categories.isEmpty ?? true) + $0.event.canonicalDay == day && !($0.event.menu.isEmpty) }.sorted { lhs, rhs in lhs.event.startTimestamp < rhs.event.startTimestamp } diff --git a/Eatery Blue/UI/MenusScreen/MenusFilterViewController.swift b/Eatery Blue/UI/MenusScreen/MenusFilterViewController.swift index a9ddc58c..c0799401 100644 --- a/Eatery Blue/UI/MenusScreen/MenusFilterViewController.swift +++ b/Eatery Blue/UI/MenusScreen/MenusFilterViewController.swift @@ -10,7 +10,7 @@ import UIKit protocol MenusFilterViewControllerDelegate: AnyObject { func menusFilterViewController(_ viewController: MenusFilterViewController, didChangeLocation filter: EateryFilter) - func menusFilterViewController(_ viewController: MenusFilterViewController, didChangeMenuType string: String) + func menusFilterViewController(_ viewController: MenusFilterViewController, didChangeMenuType eventType: EventType) } class MenusFilterViewController: UIViewController { @@ -19,7 +19,7 @@ class MenusFilterViewController: UIViewController { let west = PillFilterButtonView() let central = PillFilterButtonView() - private let currentMealType: String? + private let currentMealType: EventType? var selectedMenuIndex: Int? @@ -40,17 +40,15 @@ class MenusFilterViewController: UIViewController { updateFilterButtonsFromState(animated: false) } - init(currentMealType: String) { - // MARK: todo - This should be an enum - + init(currentMealType: EventType) { self.currentMealType = currentMealType - if currentMealType == "Breakfast" { + if currentMealType == .breakfast { selectedMenuIndex = 0 - } else if currentMealType == "Lunch" { + } else if currentMealType == .lunch { selectedMenuIndex = 1 - } else if currentMealType == "Dinner" { + } else if currentMealType == .dinner { selectedMenuIndex = 2 - } else if currentMealType == "Late Dinner" { + } else if currentMealType == .lateDinner { selectedMenuIndex = 3 } @@ -84,7 +82,7 @@ class MenusFilterViewController: UIViewController { private func setUpMealType() { if let currentMealType = currentMealType { - mealType.label.text = currentMealType + mealType.label.text = currentMealType.description } else { return mealType.label.text = "Breakfast" } @@ -170,7 +168,7 @@ extension MenusFilterViewController: UpcomingMenuPickerSheetViewControllerDelega didChangeMenuChoice string: String ) { mealType.label.text = string - if let mealType = mealType.label.text { + if let mealType = currentMealType { delegate?.menusFilterViewController(self, didChangeMenuType: mealType) } } diff --git a/Eatery Blue/UI/MenusScreen/MenusViewController.swift b/Eatery Blue/UI/MenusScreen/MenusViewController.swift index 5a025d88..af168038 100644 --- a/Eatery Blue/UI/MenusScreen/MenusViewController.swift +++ b/Eatery Blue/UI/MenusScreen/MenusViewController.swift @@ -19,9 +19,9 @@ class MenusViewController: UIViewController { // MARK: - Properties (data) private var allEateries: [Int: [Eatery]] = [:] - private var currentMealType: String = .Eatery.mealFromTime() + private var currentMealType = EventType.mealFromTime() private let daysToShow = 7 - private var expandedEateryIds: [Int64] = [] + private var expandedEateryIds: [Int] = [] private var filter = EateryFilter() private lazy var filterController = MenusFilterViewController(currentMealType: currentMealType) private var headerHeight: CGFloat = Constants.maxHeaderHeight @@ -183,7 +183,8 @@ class MenusViewController: UIViewController { /// Load all of the eateries by the given index private func updateAllEateriesByDayFromNetworking(_ day: Int) async throws { - let eateries = Constants.isTesting ? DummyData.eateries : try await Networking.default.loadEateryByDay(day: day) + let eateries = Constants.isTesting ? Eatery.dummyEateries : try await Networking.default + .loadEateryByDay(day: day) allEateries[day] = eateries } @@ -405,19 +406,19 @@ class MenusViewController: UIViewController { // MARK: todo - This should be an enum but good for now filteredEateries = filteredEateries.filter { eatery in - if !eatery.paymentMethods.contains(.mealSwipes) { return false } + if !eatery.paymentMethods.contains(.mealSwipe) { return false } let events = eatery.events.filter { $0.canonicalDay == Day().advanced(by: selectedIndex) } // Ignore Late Lunch - if currentMealType == "Breakfast" { - return events.contains { $0.description == "Brunch" || $0.description == "Breakfast" } - } else if currentMealType == "Lunch" { - return events.contains { $0.description == "Brunch" || $0.description == "Lunch" } - } else if currentMealType == "Dinner" { - return events.contains { $0.description == "Dinner" } - } else if currentMealType == "Late Dinner" { - return events.contains { $0.description == "Late Night" } + if currentMealType == .breakfast { + return events.contains { $0.type == .brunch || $0.type == .breakfast } + } else if currentMealType == .lunch { + return events.contains { $0.type == .brunch || $0.type == .lunch } + } else if currentMealType == .dinner { + return events.contains { $0.type == .dinner } + } else if currentMealType == .lateDinner { + return events.contains { $0.type == .lateDinner } } return false @@ -428,11 +429,11 @@ class MenusViewController: UIViewController { var central: [Eatery] = [] for eatery in filteredEateries { - if eatery.campusArea == "North" { + if eatery.campusArea == .north { north.append(eatery) - } else if eatery.campusArea == "West" { + } else if eatery.campusArea == .west { west.append(eatery) - } else if eatery.campusArea == "Central" { + } else if eatery.campusArea == .central { central.append(eatery) } } @@ -527,7 +528,7 @@ extension MenusViewController { struct ExpandedEatery: Hashable { let eatery: Eatery - var selectedMealType: String? + var selectedMealType: EventType? var selectedDate: Day? var isExpanded: Bool } @@ -558,17 +559,17 @@ extension MenusViewController: UITableViewDelegate { // if we are currently passed the end date of the event, do nothing var event: Event? // Ignoring Late Lunch - if currentMealType == "Breakfast" { - event = eatery.events.first { $0.description == "Brunch" || $0.description == "Breakfast" } - } else if currentMealType == "Lunch" { - event = eatery.events.first { $0.description == "Brunch" || $0.description == "Lunch" } - } else if currentMealType == "Dinner" { - event = eatery.events.first { $0.description == "Dinner" } - } else if currentMealType == "Late Dinner" { - event = eatery.events.first { $0.description == "Late Night" } + if currentMealType == .breakfast { + event = eatery.events.first { $0.type == .brunch || $0.type == .breakfast } + } else if currentMealType == .lunch { + event = eatery.events.first { $0.type == .brunch || $0.type == .lunch } + } else if currentMealType == .dinner { + event = eatery.events.first { $0.type == .dinner } + } else if currentMealType == .lateDinner { + event = eatery.events.first { $0.type == .lateDinner } } - guard let event, event.endDate > Date() else { return } + guard let event, event.endTimestamp > Date() else { return } if expandedEateryIds.contains(expandedEatery.eatery.id) { expandedEateryIds.removeAll(where: { $0 == expandedEatery.eatery.id }) @@ -702,8 +703,8 @@ extension MenusViewController: MenusFilterViewControllerDelegate { } } - func menusFilterViewController(_: MenusFilterViewController, didChangeMenuType string: String) { - currentMealType = string + func menusFilterViewController(_: MenusFilterViewController, didChangeMenuType eventType: EventType) { + currentMealType = eventType if let eateries = allEateries[selectedIndex], !eateries.isEmpty { applySnapshot() } else { @@ -714,13 +715,13 @@ extension MenusViewController: MenusFilterViewControllerDelegate { } } - if currentMealType == "Breakfast" { + if currentMealType == .breakfast { filterController.selectedMenuIndex = 0 - } else if currentMealType == "Lunch" { + } else if currentMealType == .lunch { filterController.selectedMenuIndex = 1 - } else if currentMealType == "Dinner" { + } else if currentMealType == .dinner { filterController.selectedMenuIndex = 2 - } else if currentMealType == "Late Dinner" { + } else if currentMealType == .lateDinner { filterController.selectedMenuIndex = 3 } } diff --git a/Eatery Blue/UI/PaymentMethodsFilterSheet/PaymentMethodsFilterSheetViewController.swift b/Eatery Blue/UI/PaymentMethodsFilterSheet/PaymentMethodsFilterSheetViewController.swift index f04ce420..e837ea45 100644 --- a/Eatery Blue/UI/PaymentMethodsFilterSheet/PaymentMethodsFilterSheetViewController.swift +++ b/Eatery Blue/UI/PaymentMethodsFilterSheet/PaymentMethodsFilterSheetViewController.swift @@ -51,10 +51,10 @@ class PaymentMethodsFilterSheetViewController: SheetViewController { stack.addArrangedSubview(mealSwipesView) mealSwipesView.label.text = "Meal swipes" mealSwipesView.tap { [self] _ in - if selectedPaymentMethods.contains(.mealSwipes) { - selectedPaymentMethods.remove(.mealSwipes) + if selectedPaymentMethods.contains(.mealSwipe) { + selectedPaymentMethods.remove(.mealSwipe) } else { - selectedPaymentMethods.insert(.mealSwipes) + selectedPaymentMethods.insert(.mealSwipe) } updatePaymentMethodViewsFromState(animated: true) @@ -75,12 +75,12 @@ class PaymentMethodsFilterSheetViewController: SheetViewController { stack.addArrangedSubview(cashOrCreditView) cashOrCreditView.label.text = "Cash or credit" cashOrCreditView.tap { [self] _ in - if selectedPaymentMethods.contains(.cash), selectedPaymentMethods.contains(.credit) { + if selectedPaymentMethods.contains(.cash), selectedPaymentMethods.contains(.card) { selectedPaymentMethods.remove(.cash) - selectedPaymentMethods.remove(.credit) + selectedPaymentMethods.remove(.card) } else { selectedPaymentMethods.insert(.cash) - selectedPaymentMethods.insert(.credit) + selectedPaymentMethods.insert(.card) } updatePaymentMethodViewsFromState(animated: true) @@ -119,7 +119,7 @@ class PaymentMethodsFilterSheetViewController: SheetViewController { return } - if selectedPaymentMethods.contains(.mealSwipes) { + if selectedPaymentMethods.contains(.mealSwipe) { mealSwipesView.imageView.image = UIImage(named: "MealSwipesSelected") mealSwipesView.label.textColor = UIColor.Eatery.blue } else { @@ -135,7 +135,7 @@ class PaymentMethodsFilterSheetViewController: SheetViewController { brbsView.label.textColor = UIColor.Eatery.secondaryText } - if selectedPaymentMethods.contains(.cash), selectedPaymentMethods.contains(.credit) { + if selectedPaymentMethods.contains(.cash), selectedPaymentMethods.contains(.card) { cashOrCreditView.imageView.image = UIImage(named: "CashSelected") cashOrCreditView.label.textColor = UIColor.Eatery.green } else { diff --git a/Eatery Blue/UI/PaymentMethodsSheet/PaymentMethodsSheetViewController.swift b/Eatery Blue/UI/PaymentMethodsSheet/PaymentMethodsSheetViewController.swift index 373c5ed1..67f68e83 100644 --- a/Eatery Blue/UI/PaymentMethodsSheet/PaymentMethodsSheetViewController.swift +++ b/Eatery Blue/UI/PaymentMethodsSheet/PaymentMethodsSheetViewController.swift @@ -73,7 +73,7 @@ class PaymentMethodsSheetViewController: SheetViewController { func setPaymentMethods(_ paymentMethods: Set) { self.paymentMethods = paymentMethods - mealSwipesImageView.tintColor = paymentMethods.contains(.mealSwipes) + mealSwipesImageView.tintColor = paymentMethods.contains(.mealSwipe) ? UIColor.Eatery.blue : UIColor.Eatery.secondaryText @@ -81,7 +81,7 @@ class PaymentMethodsSheetViewController: SheetViewController { ? UIColor.Eatery.red : UIColor.Eatery.secondaryText - cashOrCardImageView.tintColor = paymentMethods.contains(.cash) || paymentMethods.contains(.credit) + cashOrCardImageView.tintColor = paymentMethods.contains(.cash) || paymentMethods.contains(.card) ? UIColor.Eatery.green : UIColor.Eatery.secondaryText @@ -125,7 +125,7 @@ class PaymentMethodsSheetViewController: SheetViewController { private func getAttributedStrings(_ paymentMethods: Set) -> [NSAttributedString] { var result: [NSAttributedString] = [] - if paymentMethods.contains(.mealSwipes) { + if paymentMethods.contains(.mealSwipe) { let attributedString = NSMutableAttributedString() let attachment = NSTextAttachment( image: UIImage(named: "MealSwipes")?.withRenderingMode(.alwaysTemplate), @@ -155,7 +155,7 @@ class PaymentMethodsSheetViewController: SheetViewController { result.append(attributedString) } - if paymentMethods.contains(.cash) || paymentMethods.contains(.credit) { + if paymentMethods.contains(.cash) || paymentMethods.contains(.card) { let attributedString = NSMutableAttributedString() let attachment = NSTextAttachment( image: UIImage(named: "Cash")?.withRenderingMode(.alwaysTemplate), diff --git a/Eatery Blue/UI/ReportIssueScreen/ReportIssueViewController.swift b/Eatery Blue/UI/ReportIssueScreen/ReportIssueViewController.swift index 548b3489..61c7e3fe 100644 --- a/Eatery Blue/UI/ReportIssueScreen/ReportIssueViewController.swift +++ b/Eatery Blue/UI/ReportIssueScreen/ReportIssueViewController.swift @@ -41,9 +41,9 @@ class ReportIssueViewController: UIViewController { !isSubmitting && selectedIssueType != nil && !issueDescriptionView.textView.text.isEmpty } - private var eateryId: Int64? + private var eateryId: Int? - init(eateryId: Int64? = nil) { + init(eateryId: Int? = nil) { self.eateryId = eateryId super.init(nibName: nil, bundle: nil) } diff --git a/Eatery Blue/UI/WaitTimesSheet/WaitTimesSheetViewController.swift b/Eatery Blue/UI/WaitTimesSheet/WaitTimesSheetViewController.swift index cc8e56c8..f6f2e8d2 100644 --- a/Eatery Blue/UI/WaitTimesSheet/WaitTimesSheetViewController.swift +++ b/Eatery Blue/UI/WaitTimesSheet/WaitTimesSheetViewController.swift @@ -56,7 +56,7 @@ class WaitTimesSheetViewController: SheetViewController { updateWaitTimeLabel() } - func setUp(_ eateryId: Int64, _ waitTimes: WaitTimes, events: [Event], day: Day = Day()) { + func setUp(_ eateryId: Int, _ waitTimes: WaitTimes, events: [Event], day: Day = Day()) { self.waitTimes = waitTimes self.day = day self.events = events diff --git a/Eatery Blue/main.swift b/Eatery Blue/main.swift index 0c669ce0..78297203 100644 --- a/Eatery Blue/main.swift +++ b/Eatery Blue/main.swift @@ -69,7 +69,7 @@ struct EateryBlue: ParsableCommand { } extension Networking { - static var `default` = Networking(fetchUrl: URL(string: "\(EateryEnvironment.baseURL)/eatery/")!) + static var `default` = Networking(fetchUrl: URL(string: EateryEnvironment.baseURL)!) } extension LocationManager { diff --git a/EateryModel/Sources/EateryModel/DummyData.swift b/EateryModel/Sources/EateryModel/DummyData.swift new file mode 100644 index 00000000..94c48b8b --- /dev/null +++ b/EateryModel/Sources/EateryModel/DummyData.swift @@ -0,0 +1,227 @@ +// +// DummyData.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/29/26. +// + +import Foundation + +public extension Eatery { + static let dummyEateries: [Eatery] = [ + // MARK: - Mac's Café (Statler Hall) + + Eatery( + id: 1, + cornellId: 1001, + announcements: ["Smoothie bar is open until 2pm!"], + name: "Mac's Café", + shortName: "Mac's", + about: + "Mac's Café offers made-to-order salads, sandwiches, and flatbreads in Statler Hall.", + shortAbout: "Salads, sandwiches, and flatbreads.", + cornellDining: true, + menuSummary: "Flatbreads • Salads • Smoothies", + imageUrl: "https://example.com/macs.jpg", + campusArea: CampusArea.central, + onlineOrderUrl: "https://get.cbord.com/cornell", + contactPhone: "607-255-0000", + contactEmail: "dining@cornell.edu", + latitude: 42.4446, + longitude: -76.4823, + location: "Statler Hall", + paymentMethods: [.brbs, .card], + eateryTypes: [.cafe], + createdAt: Date(), + events: [ + Event( + id: 101, + type: .lunch, + startTimestamp: Date(), + endTimestamp: Date().addingTimeInterval(3600 * 4), + upvotes: 24, + downvotes: 2, + createdAt: Date(), + eateryId: 1, + menu: [ + MenuCategory( + id: 1001, + name: "Flatbreads", + createdAt: Date(), + eventId: 101, + items: [ + MenuItem( + id: 1, + name: "Buffalo Chicken Flatbread", + basePrice: 8.50, + createdAt: Date(), + categoryId: 1001, + dietaryPreferences: [], + allergens: [ + Allergen(name: "Gluten"), + Allergen(name: "Dairy") + ] + ), + MenuItem( + id: 2, + name: "Margherita Flatbread", + basePrice: 7.50, + createdAt: Date(), + categoryId: 1001, + dietaryPreferences: [ + DietaryPreference(name: "Vegetarian") + ], + allergens: [ + Allergen(name: "Gluten"), + Allergen(name: "Dairy") + ] + ) + ] + ) + ] + ) + ] + ), + + // MARK: - The Terrace (Statler Hall) + + Eatery( + id: 2, + cornellId: 1002, + announcements: [], + name: "The Terrace", + shortName: "Terrace", + about: + "The Terrace Restaurant serves burritos, bowls, and salads with a view.", + shortAbout: "Burritos, bowls, and salads.", + cornellDining: true, + menuSummary: "Burritos • Pho • Salads", + imageUrl: "https://example.com/terrace.jpg", + campusArea: .central, + onlineOrderUrl: nil, + contactPhone: "607-255-0000", + contactEmail: "dining@cornell.edu", + latitude: 42.4447, + longitude: -76.4825, + location: "Statler Hall", + paymentMethods: [.brbs, .card], + eateryTypes: [.cafe], + createdAt: Date(), + events: [ + Event( + id: 201, + type: .lunch, + startTimestamp: Date(), + endTimestamp: Date().addingTimeInterval(3600 * 3), + upvotes: 45, + downvotes: 1, + createdAt: Date(), + eateryId: 2, + menu: [ + MenuCategory( + id: 2001, + name: "Burrito Station", + createdAt: Date(), + eventId: 201, + items: [ + MenuItem( + id: 3, + name: "Chicken Burrito", + basePrice: 9.00, + createdAt: Date(), + categoryId: 2001, + dietaryPreferences: [], + allergens: [Allergen(name: "Gluten")] + ), + MenuItem( + id: 4, + name: "Tofu Bowl", + basePrice: 8.50, + createdAt: Date(), + categoryId: 2001, + dietaryPreferences: [ + DietaryPreference(name: "Vegan") + ], + allergens: [Allergen(name: "Soy")] + ) + ] + ) + ] + ) + ] + ), + + // MARK: - Rose House Dining Room (West Campus) + + Eatery( + id: 3, + cornellId: 1003, + announcements: ["Chef's Table tonight: Pasta Night"], + name: "Rose House Dining Room", + shortName: "Rose", + about: + "All-you-care-to-eat dining room located in Flora Rose House on West Campus.", + shortAbout: "West Campus Dining Hall.", + cornellDining: true, + menuSummary: "Hot Traditional • Grill • Salad Bar", + imageUrl: "https://example.com/rose.jpg", + campusArea: .west, + onlineOrderUrl: nil, + contactPhone: nil, + contactEmail: nil, + latitude: 42.4460, + longitude: -76.4900, + location: "Flora Rose House", + paymentMethods: [.mealSwipe, .brbs], + eateryTypes: [.diningRoom], + createdAt: Date(), + events: [ + Event( + id: 301, + type: .dinner, + startTimestamp: Date().addingTimeInterval(3600 * 6), + endTimestamp: Date().addingTimeInterval(3600 * 9), + upvotes: 12, + downvotes: 5, + createdAt: Date(), + eateryId: 3, + menu: [ + MenuCategory( + id: 3001, + name: "Chef's Table", + createdAt: Date(), + eventId: 301, + items: [ + MenuItem( + id: 5, + name: "Penne alla Vodka", + basePrice: nil, + createdAt: Date(), + categoryId: 3001, + dietaryPreferences: [ + DietaryPreference(name: "Vegetarian") + ], + allergens: [ + Allergen(name: "Gluten"), + Allergen(name: "Dairy") + ] + ), + MenuItem( + id: 6, + name: "Garlic Bread", + basePrice: nil, + createdAt: Date(), + categoryId: 3001, + dietaryPreferences: [ + DietaryPreference(name: "Vegetarian") + ], + allergens: [Allergen(name: "Gluten")] + ) + ] + ) + ] + ) + ] + ) + ] +} diff --git a/EateryModel/Sources/EateryModel/Extensions/DateDecodingStrategy+Extension.swift b/EateryModel/Sources/EateryModel/Extensions/DateDecodingStrategy+Extension.swift new file mode 100644 index 00000000..4975655b --- /dev/null +++ b/EateryModel/Sources/EateryModel/Extensions/DateDecodingStrategy+Extension.swift @@ -0,0 +1,30 @@ +// +// DateDecodingStrategy+Extension.swift +// Eatery Blue +// +// Created by Peter Bidoshi on 2/22/26. +// + +import Foundation + +extension JSONDecoder.DateDecodingStrategy { + static let fracSecondsISO8601 = custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + let formatter = ISO8601DateFormatter() + + // Strategy 1: Try with milliseconds + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = formatter.date(from: dateString) { return date } + + // Strategy 2: Try without milliseconds + formatter.formatOptions = [.withInternetDateTime] + if let date = formatter.date(from: dateString) { return date } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unrecognized date format: \(dateString)" + ) + } +} diff --git a/EateryModel/Sources/EateryModel/Model/CampusArea.swift b/EateryModel/Sources/EateryModel/Model/CampusArea.swift new file mode 100644 index 00000000..038a83f6 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/CampusArea.swift @@ -0,0 +1,22 @@ +// +// CampusArea.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +public enum CampusArea: String, Codable, Hashable { + case east = "EAST" + + case west = "WEST" + + case north = "NORTH" + + case central = "CENTRAL" + + case collegetown = "COLLEGETOWN" + + case south = "SOUTH" + + case none = "NONE" +} diff --git a/EateryModel/Sources/EateryModel/Model/Eatery.swift b/EateryModel/Sources/EateryModel/Model/Eatery.swift index 74cdcb16..8d7f7fbc 100644 --- a/EateryModel/Sources/EateryModel/Model/Eatery.swift +++ b/EateryModel/Sources/EateryModel/Model/Eatery.swift @@ -2,122 +2,71 @@ // Eatery.swift // Eatery Blue // -// Created by William Ma on 12/22/21. +// Created by Peter Bidoshi on 01/29/26. // import CoreLocation import Foundation public struct Eatery: Codable, Hashable { + public let id: Int - public let alerts: [EateryAlert] + let cornellId: Int? - public let campusArea: String? + public let announcements: [String] - public let events: [Event] + public let name: String - public let id: Int64 + let shortName: String - public let imageUrl: URL? + let about: String - public let latitude: Double? + public let shortAbout: String - public let locationDescription: String? + let cornellDining: Bool - public let longitude: Double? + public let menuSummary: String - public let menuSummary: String? + public let imageUrl: String - public let name: String + public let campusArea: CampusArea - public let onlineOrderUrl: URL? - - public let paymentMethods: Set - - public let waitTimesByDay: [Day: WaitTimes] - - public init( - alerts: [EateryAlert] = [], - campusArea: String? = nil, - events: [Event] = [], - id: Int64, - imageUrl: URL? = nil, - latitude: Double? = nil, - locationDescription: String? = nil, - longitude: Double? = nil, - menuSummary: String? = nil, - name: String, - onlineOrderUrl: URL? = nil, - paymentMethods: Set = [], - waitTimesByDay: [Day: WaitTimes] = [:] - ) { - self.alerts = alerts - self.campusArea = campusArea - self.events = events - self.id = id - self.imageUrl = imageUrl - self.latitude = latitude - self.locationDescription = locationDescription - self.longitude = longitude - self.menuSummary = menuSummary - self.name = name - self.onlineOrderUrl = onlineOrderUrl - self.paymentMethods = paymentMethods - self.waitTimesByDay = waitTimesByDay - } + public let onlineOrderUrl: String? -} + let contactPhone: String? -public extension Eatery { + let contactEmail: String? - func walkTime(userLocation: CLLocation?) -> TimeInterval? { - guard let latitude = latitude, let longitude = longitude, let userLocation = userLocation else { - return nil - } + public let latitude: Float - let distance = userLocation.distance(from: CLLocation(latitude: latitude, longitude: longitude)) + public let longitude: Float - // https://en.wikipedia.org/wiki/Preferred_walking_speed - let walkingSpeed = 1.42 - return distance / walkingSpeed - } + public let location: String - func waitTime(date: Date) -> WaitTimeSample? { - guard let waitTimes = waitTimesByDay[Day(date: date)] else { - return nil - } + public let paymentMethods: [PaymentMethod] - return waitTimes.sample(at: date) - } + public let eateryTypes: [EateryType] - /// Estimate the time to get food at an eatery, taking into the account the walk time when calculating the wait - /// time. - /// - /// If the walk time cannot be estimated, this function returns the wait time at the departure date. - func timingInfo(userLocation: CLLocation?, departureDate: Date) -> (walkTime: TimeInterval?, waitTime: WaitTimeSample?) { - if let walkTime = walkTime(userLocation: userLocation) { - return (walkTime: walkTime, waitTime: waitTime(date: departureDate + walkTime)) - } else { - return (walkTime: nil, waitTime: waitTime(date: departureDate)) - } - } + let createdAt: Date + + public let events: [Event] +} - /// Expected time to get food at an eatery, taking into the account the walk time when calculating the wait time. - func expectedTotalTime(userLocation: CLLocation?, departureDate: Date) -> TimeInterval? { - let timingInfo = timingInfo(userLocation: userLocation, departureDate: departureDate) - if timingInfo.walkTime == nil, timingInfo.waitTime == nil { +public extension Eatery { + func walkTime(userLocation: CLLocation?) -> TimeInterval? { + guard let userLocation = userLocation else { return nil } - let walkTime = timingInfo.walkTime ?? 0 - let waitTime = timingInfo.waitTime?.expected ?? 0 - return walkTime + waitTime - } + let distance = userLocation.distance(from: CLLocation(latitude: Double(latitude), longitude: Double(longitude))) + // https://en.wikipedia.org/wiki/Preferred_walking_speed + let walkingSpeed = 1.42 + return distance / walkingSpeed + } } public extension Eatery { - var status: EateryStatus { EateryStatus(events) } @@ -125,5 +74,4 @@ public extension Eatery { var isOpen: Bool { status.isOpen } - } diff --git a/EateryModel/Sources/EateryModel/Model/EateryAlert.swift b/EateryModel/Sources/EateryModel/Model/EateryAlert.swift deleted file mode 100644 index 9deefcf7..00000000 --- a/EateryModel/Sources/EateryModel/Model/EateryAlert.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// EateryAlert.swift -// -// -// Created by William Ma on 1/28/22. -// - -import Foundation - -public struct EateryAlert: Codable, Hashable { - - public let description: String? - - public let endTimestamp: Int - - public let id: Int64 - - public let startTimestamp: Int - - public init( - description: String? = nil, - endTimestamp: Int, - id: Int64, - startTimestamp: Int - ) { - self.description = description - self.endTimestamp = endTimestamp - self.id = id - self.startTimestamp = startTimestamp - } - -} - -public extension EateryAlert { - - var startDate: Date { - Date(timeIntervalSince1970: TimeInterval(startTimestamp)) - } - - var endDate: Date { - Date(timeIntervalSince1970: TimeInterval(endTimestamp)) - } - -} diff --git a/EateryModel/Sources/EateryModel/Model/EateryStatus.swift b/EateryModel/Sources/EateryModel/Model/EateryStatus.swift index 374ba6fe..cc0e4077 100644 --- a/EateryModel/Sources/EateryModel/Model/EateryStatus.swift +++ b/EateryModel/Sources/EateryModel/Model/EateryStatus.swift @@ -8,8 +8,11 @@ import Foundation public enum EateryStatus { - - public static func index(_ events: [Event], filter isIncluded: (Event) -> Bool, min isLessThan: (Event, Event) -> Bool) -> Int? { + public static func index( + _ events: [Event], + filter isIncluded: (Event) -> Bool, + min isLessThan: (Event, Event) -> Bool + ) -> Int? { events.enumerated().filter { isIncluded($0.element) }.min { @@ -19,7 +22,8 @@ public enum EateryStatus { public static func indexOfCurrentEvent(_ events: [Event], date: Date = Date(), on day: Day? = nil) -> Int? { events.firstIndex { event in - event.startDate <= date && date <= event.endDate && (day != nil ? event.canonicalDay == day : true) + event.startTimestamp <= date && date <= event + .endTimestamp && (day != nil ? event.canonicalDay == day : true) } } @@ -33,9 +37,9 @@ public enum EateryStatus { public static func indexOfNextEvent(_ events: [Event], date: Date = Date(), on day: Day? = nil) -> Int? { index(events) { event in - date <= event.startDate && (day != nil ? event.canonicalDay == day : true) + date <= event.startTimestamp && (day != nil ? event.canonicalDay == day : true) } min: { lhs, rhs in - lhs.startDate < rhs.startDate + lhs.startTimestamp < rhs.startTimestamp } } @@ -49,9 +53,9 @@ public enum EateryStatus { public static func indexOfPreviousEvent(_ events: [Event], date: Date = Date(), on day: Day? = nil) -> Int? { index(events) { event in - event.endDate <= date && (day != nil ? event.canonicalDay == day : true) + event.endTimestamp <= date && (day != nil ? event.canonicalDay == day : true) } min: { lhs, rhs in - rhs.endDate < lhs.endDate + rhs.endTimestamp < lhs.endTimestamp } } @@ -78,13 +82,11 @@ public enum EateryStatus { case openingSoon(Event) public init(_ events: [Event], date: Date = Date()) { - let timestamp = date.timeIntervalSince1970 - if let event = EateryStatus.currentEvent(events, date: date) { // The eatery is open. Is it closing soon? - let timeUntilClose = event.endTimestamp - timestamp + let timeUntilClose = event.endTimestamp.distance(to: date) - if timeUntilClose <= 60 * 60 { + if abs(timeUntilClose) <= 60 * 60 { self = .closingSoon(event) } else { self = .open(event) @@ -92,9 +94,9 @@ public enum EateryStatus { } else if let event = EateryStatus.nextEvent(events, date: date) { // The eatery is closed. Is it opening soon? - let timeUntilOpen = event.startTimestamp - timestamp + let timeUntilOpen = event.startTimestamp.distance(to: date) - if timeUntilOpen <= 60 * 60 { + if abs(timeUntilOpen) <= 60 * 60 { self = .openingSoon(event) } else { self = .closed @@ -102,7 +104,6 @@ public enum EateryStatus { } else { self = .closed - } } @@ -112,5 +113,4 @@ public enum EateryStatus { case .closed, .openingSoon: return false } } - } diff --git a/EateryModel/Sources/EateryModel/Model/EateryType.swift b/EateryModel/Sources/EateryModel/Model/EateryType.swift new file mode 100644 index 00000000..c44ec9d3 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/EateryType.swift @@ -0,0 +1,41 @@ +// +// EateryType.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +public enum EateryType: String, Codable, Hashable { + case diningRoom = "DINING_ROOM" + + case cafe = "CAFE" + + case coffeeShop = "COFFEE_SHOP" + + case foodCourt = "FOOD_COURT" + + case convenienceStore = "CONVENIENCE_STORE" + + case cart = "CART" + + case general = "GENERAL" + + public var description: String { + switch self { + case .diningRoom: + return "Dining Room" + case .cafe: + return "Cafe" + case .coffeeShop: + return "Coffee Shop" + case .foodCourt: + return "Food Court" + case .convenienceStore: + return "Convenience Store" + case .cart: + return "Foot Cart" + case .general: + return "Other Eatery" + } + } +} diff --git a/EateryModel/Sources/EateryModel/Model/Event.swift b/EateryModel/Sources/EateryModel/Model/Event.swift deleted file mode 100644 index ad7234e2..00000000 --- a/EateryModel/Sources/EateryModel/Model/Event.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Event.swift -// -// -// Created by William Ma on 1/12/22. -// - -import Foundation - -public struct Event: Codable, Hashable { - - public var canonicalDay: Day - - public var description: String? - - public var endTimestamp: TimeInterval - - public var menu: Menu? - - public var startTimestamp: TimeInterval - - public init( - canonicalDay: Day, - description: String? = nil, - endTimestamp: TimeInterval, - menu: Menu? = nil, - startTimestamp: TimeInterval - ) { - self.canonicalDay = canonicalDay - self.startTimestamp = startTimestamp - self.endTimestamp = endTimestamp - self.description = description - self.menu = menu - } - -} - -extension Event { - - public var endDate: Date { - Date(timeIntervalSince1970: endTimestamp) - } - - public var startDate: Date { - Date(timeIntervalSince1970: startTimestamp) - } - -} diff --git a/EateryModel/Sources/EateryModel/Model/Event/Allergen.swift b/EateryModel/Sources/EateryModel/Model/Event/Allergen.swift new file mode 100644 index 00000000..68a4c245 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/Allergen.swift @@ -0,0 +1,10 @@ +// +// Allergen.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +public struct Allergen: Codable, Hashable { + public let name: String +} diff --git a/EateryModel/Sources/EateryModel/Model/Event/DietaryPreference.swift b/EateryModel/Sources/EateryModel/Model/Event/DietaryPreference.swift new file mode 100644 index 00000000..75350f01 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/DietaryPreference.swift @@ -0,0 +1,10 @@ +// +// DietaryPreference.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +public struct DietaryPreference: Codable, Hashable { + public let name: String +} diff --git a/EateryModel/Sources/EateryModel/Model/Event/Event.swift b/EateryModel/Sources/EateryModel/Model/Event/Event.swift new file mode 100644 index 00000000..6d24c460 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/Event.swift @@ -0,0 +1,34 @@ +// +// Event.swift +// +// +// Created by William Ma on 1/12/22. +// + +import Foundation + +public struct Event: Codable, Hashable { + let id: Int + + public let type: EventType + + public let startTimestamp: Date + + public let endTimestamp: Date + + let upvotes: Int + + let downvotes: Int + + let createdAt: Date + + let eateryId: Int + + public let menu: [MenuCategory] +} + +public extension Event { + var canonicalDay: Day { + Day(date: startTimestamp) + } +} diff --git a/EateryModel/Sources/EateryModel/Model/Event/EventType.swift b/EateryModel/Sources/EateryModel/Model/Event/EventType.swift new file mode 100644 index 00000000..193ab660 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/EventType.swift @@ -0,0 +1,86 @@ +// +// EventType.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +import Foundation + +public enum EventType: String, Codable, CaseIterable { + case availableAllDay = "AVAILABLE_ALL_DAY" + + case breakfast = "BREAKFAST" + + case brunch = "BRUNCH" + + case dinner = "DINNER" + + case empty = "EMPTY" + + case lateDinner = "LATE_NIGHT" + + case lateLunch = "LATE_LUNCH" + + case lunch = "LUNCH" + + case open = "OPEN" + + case general = "GENERAL" // Used for dummy event for Cafe eateries (ones with no menus) + + case pants = "PANTS" // Pants + + public var description: String { + switch self { + case .availableAllDay: + return "Available All Day" + case .breakfast: + return "Breakfast" + case .brunch: + return "Brunch" + case .dinner: + return "Dinner" + case .empty: + return "No Event" + case .lateDinner: + return "Late Dinner" + case .lateLunch: + return "Late Lunch" + case .lunch: + return "Lunch" + case .open: + return "Open" + case .general: + return "General" + case .pants: + return "Pants" + } + } + + /** + Determines the correct meal based on the time of day. + + This function gets the time of day from Date and returns a string of the meal type. If it is a specific meal time, + that meal time is returned, and if it is not a specific meal time, the next meal time is returned. + + - Returns: If it is 10:30 pm - 10:29 am inclusive, breakfest is returned. If it is 10:30 am - 2:29 pm inclusive, + lunch is returned. If it is 2:30 pm - 8:29 pm inclusive, dinner is returned. + If it is 8:30 pm - 10:29 pm inclusive, late dinner is returned. + */ + public static func mealFromTime() -> EventType { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH" + guard let hour = Int(dateFormatter.string(from: Date())) else { return .breakfast } + dateFormatter.dateFormat = "mm" + guard let minute = Int(dateFormatter.string(from: Date())) else { return .breakfast } + if hour < 10 || hour == 10 && minute < 30 || hour >= 23 || hour == 22 && minute >= 30 { + return .breakfast + } else if hour == 10 && minute >= 30 || hour > 10 && hour < 14 || hour == 14 && minute < 30 { + return .lunch + } else if hour == 14 && minute >= 30 || hour > 14 && hour < 20 || hour == 20 && minute < 30 { + return .dinner + } else { + return .lateDinner + } + } +} diff --git a/EateryModel/Sources/EateryModel/Model/Event/MenuCategory.swift b/EateryModel/Sources/EateryModel/Model/Event/MenuCategory.swift new file mode 100644 index 00000000..8298c495 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/MenuCategory.swift @@ -0,0 +1,20 @@ +// +// MenuCategory.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +import Foundation + +public struct MenuCategory: Codable, Hashable { + let id: Int + + public let name: String + + let createdAt: Date + + let eventId: Int + + public let items: [MenuItem] +} diff --git a/EateryModel/Sources/EateryModel/Model/Event/MenuItem.swift b/EateryModel/Sources/EateryModel/Model/Event/MenuItem.swift new file mode 100644 index 00000000..69ffbc66 --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/Event/MenuItem.swift @@ -0,0 +1,24 @@ +// +// MenuItem.swift +// EateryModel +// +// Created by Peter Bidoshi on 1/28/26. +// + +import Foundation + +public struct MenuItem: Codable, Hashable { + let id: Int + + public let name: String + + public let basePrice: Double? + + let createdAt: Date + + let categoryId: Int + + public let dietaryPreferences: [DietaryPreference] + + public let allergens: [Allergen] +} diff --git a/EateryModel/Sources/EateryModel/Model/Menu.swift b/EateryModel/Sources/EateryModel/Model/Menu.swift deleted file mode 100644 index 8ad0e003..00000000 --- a/EateryModel/Sources/EateryModel/Model/Menu.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Menu.swift -// -// -// Created by William Ma on 1/12/22. -// - -import Foundation - -public struct Menu: Codable, Hashable { - - public let categories: [MenuCategory] - - public init(categories: [MenuCategory]) { - self.categories = categories - } - -} diff --git a/EateryModel/Sources/EateryModel/Model/MenuCategory.swift b/EateryModel/Sources/EateryModel/Model/MenuCategory.swift deleted file mode 100644 index 5df37a0e..00000000 --- a/EateryModel/Sources/EateryModel/Model/MenuCategory.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MenuCategory.swift -// -// -// Created by William Ma on 1/12/22. -// - -import Foundation - -public struct MenuCategory: Codable, Hashable { - - public let category: String - - public let items: [MenuItem] - - public init(category: String, items: [MenuItem]) { - self.category = category - self.items = items - } - -} diff --git a/EateryModel/Sources/EateryModel/Model/MenuItem.swift b/EateryModel/Sources/EateryModel/Model/MenuItem.swift deleted file mode 100644 index 196be770..00000000 --- a/EateryModel/Sources/EateryModel/Model/MenuItem.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// MenuItem.swift -// -// -// Created by William Ma on 1/12/22. -// - -import Foundation - -public struct MenuItem: Codable, Hashable { - - public let id: Int - - public let description: String? - - public let healthy: Bool - - public let name: String - - public let price: Double? - - public let allergens: [String] - - public let dietaryPreferences: [String] - - public init(id: Int, description: String? = nil, healthy: Bool, name: String, price: Double? = nil, allergens: [String] = [], dietaryPreferences: [String] = []) { - self.id = id - self.description = description - self.healthy = healthy - self.name = name - self.price = price - self.allergens = allergens - self.dietaryPreferences = dietaryPreferences - } - -} diff --git a/EateryModel/Sources/EateryModel/Model/PaymentMethod.swift b/EateryModel/Sources/EateryModel/Model/PaymentMethod.swift index bd0c5e52..7c8d8b0d 100644 --- a/EateryModel/Sources/EateryModel/Model/PaymentMethod.swift +++ b/EateryModel/Sources/EateryModel/Model/PaymentMethod.swift @@ -1,20 +1,35 @@ // // PaymentMethod.swift -// // -// Created by William Ma on 1/12/22. +// +// Created by Peter Bidoshi on 1/28/26. // import Foundation -public enum PaymentMethod: Codable, Hashable { +public enum PaymentMethod: String, Codable, Hashable { + case mealSwipe = "MEAL_SWIPE" - case mealSwipes + case cash = "CASH" - case brbs + case card = "CARD" - case cash + case brbs = "BRB" - case credit + case free = "FREE" + public var description: String { + switch self { + case .mealSwipe: + return "Meal Swipe" + case .cash: + return "Cash" + case .card: + return "Card" + case .brbs: + return "BRBs" + case .free: + return "Free" + } + } } diff --git a/EateryModel/Sources/EateryModel/Model/VersionResponse.swift b/EateryModel/Sources/EateryModel/Model/VersionResponse.swift new file mode 100644 index 00000000..a4edd0fb --- /dev/null +++ b/EateryModel/Sources/EateryModel/Model/VersionResponse.swift @@ -0,0 +1,10 @@ +// +// VersionResponse.swift +// EateryModel +// +// Created by Peter Bidoshi on 2/22/26. +// + +public struct VersionResponse: Decodable { + public let version: String +} diff --git a/EateryModel/Sources/EateryModel/Networking/EateryAPI.swift b/EateryModel/Sources/EateryModel/Networking/EateryAPI.swift index 1e3cae08..8f0988fa 100644 --- a/EateryModel/Sources/EateryModel/Networking/EateryAPI.swift +++ b/EateryModel/Sources/EateryModel/Networking/EateryAPI.swift @@ -16,38 +16,30 @@ public struct EateryAPI { self.url = url decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .fracSecondsISO8601 } - public func eateries() async throws -> [Eatery] { + public func version() async throws -> VersionResponse { let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - var schemaApiResponse: [Schema.Eatery] = [] - do { - schemaApiResponse = try decoder.decode([Schema.Eatery].self, from: data) - } catch { - throw EateryAPIError.apiResponseError(error.localizedDescription) - } + return try decoder.decode(VersionResponse.self, from: data) + } + + public func eateries() async throws -> [Eatery] { + let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - return schemaApiResponse.map(SchemaToModel.convert) + return try decoder.decode([Eatery].self, from: data) } public func eatery() async throws -> Eatery { let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - var schemaApiResponse: Schema.Eatery - - do { - schemaApiResponse = try decoder.decode(Schema.Eatery.self, from: data) - } catch { - throw EateryAPIError.apiResponseError(error.localizedDescription) - } - return SchemaToModel.convert(schemaApiResponse) + return try decoder.decode(Eatery.self, from: data) } - public func reportError(eatery: Int64? = nil, content: String) async { + public func reportError(eatery: Int? = nil, content: String) async { struct ReportData: Codable { - let eatery: Int64? + let eatery: Int? let content: String } diff --git a/EateryModel/Sources/EateryModel/Networking/Schema.swift b/EateryModel/Sources/EateryModel/Networking/Schema.swift deleted file mode 100644 index 2755c14f..00000000 --- a/EateryModel/Sources/EateryModel/Networking/Schema.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// Schema.swift -// Eatery Blue -// -// Created by William Ma on 12/30/21. -// - -import Foundation - -enum Schema { - struct Alert: Codable { - let description: String? - - let endTimestamp: Int - - let id: Int64 - - let startTimestamp: Int - } - - struct Eatery: Codable { - let alerts: [Alert]? - - let campusArea: String? - - let events: [Schema.Event]? - - let id: Int64 - - let imageUrl: String? - - let latitude: Double? - - let location: String? - - let longitude: Double? - - let menuSummary: String? - - let name: String? - - let onlineOrderUrl: URL? - - let paymentAcceptsCash: Bool? - - let paymentAcceptsBrbs: Bool? - - let paymentAcceptsMealSwipes: Bool? - - let waitTime: [Schema.WaitTime]? - } - - struct Event: Codable { - let eatery: Int? // Need to communicate with backend about purpose of this var - - let end: Int? - - let eventDescription: String? - - let id: Int? - - let start: Int? - - let menu: [Schema.MenuCategory]? - } - - struct MenuCategory: Codable { - let category: String - - let event: Int? // Need to communciate about purpose of this var - - let id: Int? // And this - - let items: [Schema.MenuItem] - } - - struct MenuItem: Codable { - let category: Int? - - let healthy: Bool? - - let id: Int - - let name: String - - let allergens: [String] - - let dietaryPreferences: [String] - } - - struct WaitTime: Codable { - let day: String - - let eatery: Int // Not sure why this is needed - - let hour: Int - - let id: Int // Not sure why this is needed - - let trials: Int // Not sure what this is for - - let waitTimeExpected: Int - - let waitTimeHigh: Int - - let waitTimeLow: Int - } - - // MARK: - WaitTimesByDay (uncomment and modify once implemented) - -// internal struct WaitTimesByDay: Codable { -// -// internal let canonicalDate: String? -// -// internal let data: [Schema.WaitTimes]? -// -// } -} diff --git a/EateryModel/Sources/EateryModel/Networking/SchemaToModel.swift b/EateryModel/Sources/EateryModel/Networking/SchemaToModel.swift deleted file mode 100644 index a11484b1..00000000 --- a/EateryModel/Sources/EateryModel/Networking/SchemaToModel.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// SchemaToModel.swift -// Eatery Blue -// -// Created by William Ma on 12/30/21. -// - -import Foundation - -internal enum SchemaToModel { - - internal static func convert(_ schemaEatery: Schema.Eatery) -> Eatery { - Eatery( - alerts: convert(schemaEatery.alerts), - campusArea: schemaEatery.campusArea, - events: convert(schemaEatery.events), - id: schemaEatery.id, - imageUrl: convert(schemaEatery.imageUrl), - latitude: schemaEatery.latitude, - locationDescription: schemaEatery.location, - longitude: schemaEatery.longitude, - menuSummary: schemaEatery.menuSummary, - name: schemaEatery.name ?? "", - onlineOrderUrl: schemaEatery.onlineOrderUrl, - paymentMethods: convert( - acceptsBrbs: schemaEatery.paymentAcceptsBrbs, - acceptsCash: schemaEatery.paymentAcceptsCash, - acceptsMealSwipes: schemaEatery.paymentAcceptsMealSwipes - ), - waitTimesByDay: convert(schemaEatery.waitTime) - ) - } - - internal static func convert(_ schemaUrl: String?) -> URL? { - guard let url = schemaUrl else { - return nil - } - - return URL(string: url) - } - - internal static func convert(_ schemaAlerts: [Schema.Alert]?) -> [EateryAlert] { - guard let schemaAlerts = schemaAlerts else { - return [] - } - - return schemaAlerts.map(convert) - } - - internal static func convert(_ schemaAlert: Schema.Alert) -> EateryAlert { - EateryAlert( - description: schemaAlert.description, - endTimestamp: schemaAlert.endTimestamp, - id: schemaAlert.id, - startTimestamp: schemaAlert.startTimestamp - ) - } - - internal static func convert(acceptsBrbs: Bool?, acceptsCash: Bool?, acceptsMealSwipes: Bool?) -> Set { - var paymentMethods: Set = [] - - if acceptsBrbs ?? false { - paymentMethods.insert(.brbs) - } - - if acceptsCash ?? false { - paymentMethods.insert(.cash) - } - - if acceptsMealSwipes ?? false { - paymentMethods.insert(.mealSwipes) - } - - return paymentMethods - } - - internal static func convert(_ schemaEvents: [Schema.Event]?) -> [Event] { - guard let schemaEvents = schemaEvents else { - return [] - } - - return schemaEvents.compactMap(convert) - } - - internal static func convert(_ schemaEvent: Schema.Event) -> Event? { - guard let start = schemaEvent.start, let end = schemaEvent.end else { - return nil - } - - let menu: Menu? - if let schemaMenuCategories = schemaEvent.menu { - menu = Menu(categories: schemaMenuCategories.map(convert)) - } else { - menu = nil - } - - return Event( - canonicalDay: Day(date: Date(timeIntervalSince1970: TimeInterval(start))), - description: schemaEvent.eventDescription, - endTimestamp: TimeInterval(end), - menu: menu, - startTimestamp: TimeInterval(start) - ) - } - - internal static func convert(_ schemaMenuCategory: Schema.MenuCategory) -> MenuCategory { - MenuCategory( - category: schemaMenuCategory.category, - items: schemaMenuCategory.items.map(convert) - ) - } - - internal static func convert(_ schemaMenuItem: Schema.MenuItem) -> MenuItem { - MenuItem( - id: schemaMenuItem.id, - description: nil, - healthy: schemaMenuItem.healthy ?? false, - name: schemaMenuItem.name, - price: nil, - allergens: schemaMenuItem.allergens, - dietaryPreferences: schemaMenuItem.dietaryPreferences - ) - } - -// MARK: - Conversion for WaitTimesByDay (uncomment and modify once implemented) -// internal static func convert(_ schemaWaitTimesByDay: [Schema.WaitTimesByDay]?) -> [Day: WaitTimes] { -// guard let schemaWaitTimesByDay = schemaWaitTimesByDay else { -// return [:] -// } -// -// if schemaWaitTimesByDay.isEmpty { -// return [:] -// } -// -// var waitTimesByDay: [Day: WaitTimes] = [:] -// for schemaWaitTimesByDay in schemaWaitTimesByDay { -// guard let dayString = schemaWaitTimesByDay.canonicalDate, -// let day = Day(string: dayString), -// let waitTimes = schemaWaitTimesByDay.data, -// !waitTimes.isEmpty -// else { -// continue -// } -// -// waitTimesByDay[day] = WaitTimes(samples: waitTimes.map(convert), samplingMethod: .nearestNeighbor) -// } -// -// return waitTimesByDay -// } - - internal static func convert(_ schemaWaitTimes: [Schema.WaitTime]?) -> [Day: WaitTimes] { - guard let schemaWaitTimes = schemaWaitTimes else { - return [:] - } - - if schemaWaitTimes.isEmpty { - return [:] - } - - let day = Day(date: Date.now) - var waitTimesByDay: [Day: WaitTimes] = [:] - waitTimesByDay[day] = WaitTimes(samples: schemaWaitTimes.map(convert), samplingMethod: .nearestNeighbor) - - return waitTimesByDay - } - - internal static func convert(_ schemaWaitTime: Schema.WaitTime) -> WaitTimeSample { - WaitTimeSample( - timestamp: TimeInterval(Double(schemaWaitTime.hour) * 360.0), - low: TimeInterval(schemaWaitTime.waitTimeLow), - expected: TimeInterval(schemaWaitTime.waitTimeExpected), - high: TimeInterval(schemaWaitTime.waitTimeHigh) - ) - } - -} diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index c21693ae..b66e1d96 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -11,3 +11,7 @@ brew install wget cd $CI_WORKSPACE/ci_scripts wget -O ../Eatery\ Blue/Supporting/GoogleService-Info.plist "$GOOGLE_SERVICE_PLIST" wget -O ../Eatery\ Blue/Supporting/Keys.xcconfig "$KEYS" + +# Trust swiftlint +defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES +