mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-03-05 13:35:29 -05:00
Replace all instances of "RIT Dining" with "TigerDine"
The project and some files were still named that way, so that's been fixed now. The bundle ID is stuck that way forever but oh well. Nobody will see that.
This commit is contained in:
61
TigerDine/Views/Menus/MenuDietaryRestrictionsSheet.swift
Normal file
61
TigerDine/Views/Menus/MenuDietaryRestrictionsSheet.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// MenuDietaryRestrictionsSheet.swift
|
||||
// TigerDine
|
||||
//
|
||||
// Created by Campbell on 11/11/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MenuDietaryRestrictionsSheet: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@ObservedObject var dietaryRestrictionsModel: MenuDietaryRestrictionsModel
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Diet")) {
|
||||
Toggle(isOn: $dietaryRestrictionsModel.noBeef) {
|
||||
Text("No Beef")
|
||||
}
|
||||
Toggle(isOn: $dietaryRestrictionsModel.noPork) {
|
||||
Text("No Pork")
|
||||
}
|
||||
Toggle(isOn: $dietaryRestrictionsModel.isVegetarian) {
|
||||
Text("Vegetarian")
|
||||
}
|
||||
Toggle(isOn: $dietaryRestrictionsModel.isVegan) {
|
||||
Text("Vegan")
|
||||
}
|
||||
}
|
||||
Section(header: Text("Allergens")) {
|
||||
ForEach(Allergen.allCases, id: \.self) { allergen in
|
||||
Toggle(isOn: Binding(
|
||||
get: {
|
||||
dietaryRestrictionsModel.dietaryRestrictions.contains(allergen)
|
||||
},
|
||||
set: { isOn in
|
||||
if isOn {
|
||||
dietaryRestrictionsModel.dietaryRestrictions.add(allergen)
|
||||
} else {
|
||||
dietaryRestrictionsModel.dietaryRestrictions.remove(allergen)
|
||||
}
|
||||
}
|
||||
)) {
|
||||
Text(allergen.rawValue.capitalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Menu Filters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
TigerDine/Views/Menus/MenuItemView.swift
Normal file
117
TigerDine/Views/Menus/MenuItemView.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// MenuItemView.swift
|
||||
// TigerDine
|
||||
//
|
||||
// Created by Campbell on 11/6/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MenuItemView: View {
|
||||
@State var menuItem: FDMenuItem
|
||||
|
||||
private var infoString: String {
|
||||
// Calories SHOULD always be available, so start there.
|
||||
var str = "\(menuItem.calories) Cal • "
|
||||
// Price might be $0.00, so don't display it if that's the case because that's obviously wrong. RIT Dining would never give
|
||||
// us free food!
|
||||
if menuItem.price == 0.0 {
|
||||
str += "Price Unavailable"
|
||||
} else {
|
||||
str += "$\(String(format: "%.2f", menuItem.price))"
|
||||
}
|
||||
// Same with the price, the serving size might be 0 which is also wrong so don't display that.
|
||||
if menuItem.servingSize != 0 {
|
||||
str += " • \(menuItem.servingSize) \(menuItem.servingSizeUnit)"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text(menuItem.name)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Text(menuItem.category)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(infoString)
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack {
|
||||
ForEach(menuItem.dietaryMarkers, id: \.self) { dietaryMarker in
|
||||
Text(dietaryMarker)
|
||||
.foregroundStyle(Color.white)
|
||||
.font(.caption)
|
||||
.padding(5)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill({
|
||||
switch dietaryMarker {
|
||||
case "Vegan", "Vegetarian":
|
||||
return Color.green
|
||||
default:
|
||||
return Color.orange
|
||||
}
|
||||
}())
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
if !menuItem.allergens.isEmpty {
|
||||
Text("Allergens")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
Text(menuItem.allergens.joined(separator: ", "))
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text("Nutrition Facts")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
ForEach(menuItem.nutritionalEntries, id: \.self) { entry in
|
||||
HStack(alignment: .top) {
|
||||
Text(entry.type)
|
||||
Spacer()
|
||||
Text("\(String(format: "%.1f", entry.amount))\(entry.unit)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
Text("Ingredients")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
Text(menuItem.ingredients)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.navigationTitle("Details")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MenuItemView(
|
||||
menuItem: FDMenuItem(
|
||||
id: 0,
|
||||
name: "Chocolate Chip Muffin",
|
||||
exactName: "Muffin Chocolate Chip Thaw and Serve A; Case; 72 Ounce; 12",
|
||||
category: "Baked Goods",
|
||||
allergens: ["Wheat", "Gluten", "Egg", "Milk", "Soy"],
|
||||
calories: 470,
|
||||
nutritionalEntries: [FDNutritionalEntry(type: "Example", amount: 0.0, unit: "g")],
|
||||
dietaryMarkers: ["Vegetarian"],
|
||||
ingredients: "Some ingredients that you'd expect to find inside of a chocolate chip muffin",
|
||||
price: 2.79,
|
||||
servingSize: 1,
|
||||
servingSizeUnit: "Each")
|
||||
)
|
||||
}
|
||||
237
TigerDine/Views/Menus/MenuView.swift
Normal file
237
TigerDine/Views/Menus/MenuView.swift
Normal file
@@ -0,0 +1,237 @@
|
||||
//
|
||||
// MenuView.swift
|
||||
// TigerDine
|
||||
//
|
||||
// Created by Campbell on 11/3/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MenuView: View {
|
||||
@State var accountId: Int
|
||||
@State var locationId: Int
|
||||
@State private var menuItems: [FDMenuItem] = []
|
||||
@State private var searchText: String = ""
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var loadFailed: Bool = false
|
||||
@State private var rotationDegrees: Double = 0
|
||||
@State private var selectedMealPeriod: Int = 0
|
||||
@State private var openPeriods: [Int] = []
|
||||
@StateObject private var dietaryRestrictionsModel = MenuDietaryRestrictionsModel()
|
||||
@State private var showingDietaryRestrictionsSheet: Bool = false
|
||||
|
||||
private var animation: Animation {
|
||||
.linear
|
||||
.speed(0.1)
|
||||
.repeatForever(autoreverses: false)
|
||||
}
|
||||
|
||||
func getOpenPeriods() async {
|
||||
// Only run this if we haven't already gotten the open periods. This is somewhat of a bandaid solution to the issue of
|
||||
// fetching this information more than once, but hey it works!
|
||||
if openPeriods.isEmpty {
|
||||
switch await getFDMealPlannerOpenings(locationId: locationId) {
|
||||
case .success(let openingResults):
|
||||
openPeriods = openingResults.data.map { Int($0.id) }
|
||||
selectedMealPeriod = openPeriods[0]
|
||||
// Since this only runs once when the view first loads, we can safely use this to call the method to get the data
|
||||
// the first time. This also ensures that it doesn't happen until we have the opening periods collected.
|
||||
await getMenuForPeriod(mealPeriodId: selectedMealPeriod)
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
loadFailed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMenuForPeriod(mealPeriodId: Int) async {
|
||||
switch await getFDMealPlannerMenu(locationId: locationId, accountId: accountId, mealPeriodId: mealPeriodId) {
|
||||
case .success(let menus):
|
||||
menuItems = parseFDMealPlannerMenu(menu: menus)
|
||||
isLoading = false
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
loadFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
func getPriceString(price: Double) -> String {
|
||||
if price == 0.0 {
|
||||
return "Price Unavailable"
|
||||
} else {
|
||||
return "$\(String(format: "%.2f", price))"
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredMenuItems: [FDMenuItem] {
|
||||
var newItems = menuItems
|
||||
// Filter out allergens.
|
||||
newItems = newItems.filter { item in
|
||||
if !item.allergens.isEmpty {
|
||||
for allergen in item.allergens {
|
||||
if let checkingAllergen = Allergen(rawValue: allergen.lowercased()) {
|
||||
if dietaryRestrictionsModel.dietaryRestrictions.contains(checkingAllergen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// Filter down to vegetarian/vegan only if enabled.
|
||||
if dietaryRestrictionsModel.isVegetarian || dietaryRestrictionsModel.isVegan {
|
||||
newItems = newItems.filter { item in
|
||||
if dietaryRestrictionsModel.isVegetarian && (item.dietaryMarkers.contains("Vegetarian") || item.dietaryMarkers.contains("Vegan")) {
|
||||
return true
|
||||
} else if dietaryRestrictionsModel.isVegan && (item.dietaryMarkers.contains("Vegan")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Filter out pork/beef.
|
||||
if dietaryRestrictionsModel.noBeef {
|
||||
newItems = newItems.filter { item in
|
||||
item.dietaryMarkers.contains("Beef") == false
|
||||
}
|
||||
}
|
||||
if dietaryRestrictionsModel.noPork {
|
||||
newItems = newItems.filter { item in
|
||||
item.dietaryMarkers.contains("Pork") == false
|
||||
}
|
||||
}
|
||||
// Filter down to search contents.
|
||||
newItems = newItems.filter { item in
|
||||
let searchedLocations = searchText.isEmpty || item.name.localizedCaseInsensitiveContains(searchText)
|
||||
return searchedLocations
|
||||
}
|
||||
newItems.sort { firstItem, secondItem in
|
||||
return firstItem.name.localizedCaseInsensitiveCompare(secondItem.name) == .orderedAscending
|
||||
}
|
||||
return newItems
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isLoading {
|
||||
VStack {
|
||||
if loadFailed {
|
||||
Image(systemName: "wifi.exclamationmark.circle")
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
.foregroundStyle(.accent)
|
||||
Text("An error occurred while fetching the menu. Please check your network connection and try again.")
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
Image(systemName: "fork.knife.circle")
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
.foregroundStyle(.accent)
|
||||
.rotationEffect(.degrees(rotationDegrees))
|
||||
.onAppear {
|
||||
withAnimation(animation) {
|
||||
rotationDegrees = 360.0
|
||||
}
|
||||
}
|
||||
Text("One moment...")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await getOpenPeriods()
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
VStack {
|
||||
if !menuItems.isEmpty {
|
||||
List {
|
||||
Section {
|
||||
ForEach(filteredMenuItems) { item in
|
||||
NavigationLink(destination: MenuItemView(menuItem: item)) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(item.name)
|
||||
ForEach(item.dietaryMarkers, id: \.self) { dietaryMarker in
|
||||
Text(dietaryMarker)
|
||||
.foregroundStyle(Color.white)
|
||||
.font(.caption)
|
||||
.padding(5)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill({
|
||||
switch dietaryMarker {
|
||||
case "Vegan", "Vegetarian":
|
||||
return Color.green
|
||||
default:
|
||||
return Color.orange
|
||||
}
|
||||
}())
|
||||
)
|
||||
}
|
||||
}
|
||||
Text("\(item.calories) Cal • \(getPriceString(price: item.price))")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Menu")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.searchable(text: $searchText, prompt: "Search")
|
||||
} else {
|
||||
Image(systemName: "clock.badge.exclamationmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 75)
|
||||
.foregroundStyle(.accent)
|
||||
Text("No menu is available for the selected meal period today. Try selecting a different meal period.")
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Picker("Meal Period", selection: $selectedMealPeriod) {
|
||||
ForEach(openPeriods, id: \.self) { period in
|
||||
Text(fdmpMealPeriodsMap[period]!).tag(period)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "clock")
|
||||
Text("Meal Periods")
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Button(action: {
|
||||
showingDietaryRestrictionsSheet = true
|
||||
}) {
|
||||
Image(systemName: "line.3.horizontal.decrease")
|
||||
}
|
||||
if #unavailable(iOS 26.0) {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
if #available(iOS 26.0, *) {
|
||||
ToolbarSpacer(.flexible, placement: .bottomBar)
|
||||
DefaultToolbarItem(kind: .search, placement: .bottomBar)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedMealPeriod) {
|
||||
rotationDegrees = 0
|
||||
isLoading = true
|
||||
Task {
|
||||
await getMenuForPeriod(mealPeriodId: selectedMealPeriod)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingDietaryRestrictionsSheet) {
|
||||
MenuDietaryRestrictionsSheet(dietaryRestrictionsModel: dietaryRestrictionsModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MenuView(accountId: 1, locationId: 1)
|
||||
}
|
||||
Reference in New Issue
Block a user