mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2025-10-19 06:36:18 -04:00
- Visiting chef screen now shows "No visiting chefs today" instead of nothing when there are, get this, no visiting chefs today - 24-hour locations (Bytes) will no longer show as "Closing Soon" for the last 30 minutes of the day only to reset back to "Open" at midnight - The app will now display a network error instead of loading indefinitely if an error occurs while fetching dining data - Other minor spacing and layout changes
207 lines
8.6 KiB
Swift
207 lines
8.6 KiB
Swift
//
|
|
// VisitingChefs.swift
|
|
// RIT Dining
|
|
//
|
|
// Created by Campbell on 9/8/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct IdentifiableURL: Identifiable {
|
|
let id = UUID()
|
|
let url: URL
|
|
}
|
|
|
|
struct VisitingChefs: View {
|
|
@State private var locationsWithChefs: [DiningLocation] = []
|
|
@State private var isLoading: Bool = true
|
|
@State private var rotationDegrees: Double = 0
|
|
@State private var daySwitcherRotation: Double = 0
|
|
@State private var safariUrl: IdentifiableURL?
|
|
@State private var isTomorrow: Bool = false
|
|
|
|
private var animation: Animation {
|
|
.linear
|
|
.speed(0.1)
|
|
.repeatForever(autoreverses: false)
|
|
}
|
|
|
|
private let display: DateFormatter = {
|
|
let display = DateFormatter()
|
|
display.timeZone = TimeZone(identifier: "America/New_York")
|
|
display.dateStyle = .none
|
|
display.timeStyle = .short
|
|
return display
|
|
}()
|
|
|
|
// Asynchronously fetch the data for all of the locations on the given date (only ever today or tomorrow) to get the visiting chef
|
|
// information.
|
|
private func getDiningDataForDate(date: String) {
|
|
var newDiningLocations: [DiningLocation] = []
|
|
getAllDiningInfo(date: date) { result in
|
|
DispatchQueue.global().async {
|
|
switch result {
|
|
case .success(let locations):
|
|
for i in 0..<locations.locations.count {
|
|
let diningInfo = parseLocationInfo(location: locations.locations[i])
|
|
print(diningInfo.name)
|
|
DispatchQueue.global().sync {
|
|
// Only save the locations that actually have visiting chefs to avoid extra iterations later.
|
|
if let visitingChefs = diningInfo.visitingChefs, !visitingChefs.isEmpty {
|
|
newDiningLocations.append(diningInfo)
|
|
}
|
|
}
|
|
}
|
|
DispatchQueue.global().sync {
|
|
locationsWithChefs = newDiningLocations
|
|
isLoading = false
|
|
}
|
|
case .failure(let error): print(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getDiningData() {
|
|
isLoading = true
|
|
let dateString: String
|
|
if !isTomorrow {
|
|
dateString = getAPIFriendlyDateString(date: Date())
|
|
print("fetching visiting chefs for date \(dateString) (today)")
|
|
} else {
|
|
let calendar = Calendar.current
|
|
let tomorrow = calendar.date(byAdding: .day, value: 1, to: Date())!
|
|
dateString = getAPIFriendlyDateString(date: tomorrow)
|
|
print("fetching visiting chefs for date \(dateString) (tomorrow)")
|
|
}
|
|
getDiningDataForDate(date: dateString)
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading) {
|
|
HStack(alignment: .center) {
|
|
if !isTomorrow {
|
|
Text("Today's Visiting Chefs")
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
} else {
|
|
Text("Tomorrow's Visiting Chefs")
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
}
|
|
Spacer()
|
|
Button(action: {
|
|
withAnimation(Animation.linear.speed(1.5)) {
|
|
if isTomorrow {
|
|
daySwitcherRotation = 0.0
|
|
} else {
|
|
daySwitcherRotation = 180.0
|
|
}
|
|
}
|
|
isTomorrow.toggle()
|
|
getDiningData()
|
|
}) {
|
|
Image(systemName: "chevron.right.circle")
|
|
.rotationEffect(.degrees(daySwitcherRotation))
|
|
.font(.title)
|
|
}
|
|
}
|
|
if isLoading {
|
|
VStack {
|
|
Image(systemName: "fork.knife.circle")
|
|
.resizable()
|
|
.frame(width: 75, height: 75)
|
|
.foregroundStyle(.accent)
|
|
.rotationEffect(.degrees(rotationDegrees))
|
|
.onAppear {
|
|
rotationDegrees = 0.0
|
|
withAnimation(animation) {
|
|
rotationDegrees = 360.0
|
|
}
|
|
}
|
|
Text("Loading...")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 25)
|
|
} else {
|
|
if locationsWithChefs.isEmpty {
|
|
Text("No visiting chefs today")
|
|
.font(.title2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
ForEach(locationsWithChefs, id: \.self) { location in
|
|
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
|
|
VStack(alignment: .leading) {
|
|
Divider()
|
|
HStack(alignment: .center) {
|
|
Text(location.name)
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
Spacer()
|
|
Button(action: {
|
|
safariUrl = IdentifiableURL(url: URL(string: location.mapsUrl)!)
|
|
}) {
|
|
Image(systemName: "map")
|
|
.foregroundStyle(.accent)
|
|
}
|
|
}
|
|
ForEach(visitingChefs, id: \.self) { chef in
|
|
Spacer()
|
|
Text(chef.name)
|
|
.fontWeight(.semibold)
|
|
HStack(spacing: 3) {
|
|
if !isTomorrow {
|
|
switch chef.status {
|
|
case .hereNow:
|
|
Text("Here Now")
|
|
.foregroundStyle(.green)
|
|
case .gone:
|
|
Text("Left For Today")
|
|
.foregroundStyle(.red)
|
|
case .arrivingLater:
|
|
Text("Arriving Later")
|
|
.foregroundStyle(.red)
|
|
case .arrivingSoon:
|
|
Text("Arriving Soon")
|
|
.foregroundStyle(.orange)
|
|
case .leavingSoon:
|
|
Text("Leaving Soon")
|
|
.foregroundStyle(.orange)
|
|
}
|
|
} else {
|
|
Text("Arriving Tomorrow")
|
|
.foregroundStyle(.red)
|
|
}
|
|
Text("•")
|
|
.foregroundStyle(.secondary)
|
|
Text("\(display.string(from: chef.openTime)) - \(display.string(from: chef.closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text(chef.description)
|
|
}
|
|
}
|
|
.padding(.bottom, 20)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 8)
|
|
}
|
|
.sheet(item: $safariUrl) { url in
|
|
SafariView(url: url.url)
|
|
}
|
|
.onAppear {
|
|
getDiningData()
|
|
}
|
|
.refreshable {
|
|
getDiningData()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
VisitingChefs()
|
|
}
|