RIT-Dining/RIT Dining/DetailView.swift
NinjaCheetah 24b71b7b3f
Improved visiting chef and daily specials information
Reworked the detail view for locations to use the screen space more effectively and show the day's visiting chefs and daily specials, as well as the hours for the entire week.
The visiting chef screen has also been massively reworked to show all visiting chefs under what location they're in, with their times for the day and an indicator marking their status (one of: "Here Now", "Arriving Later", "Ariving Soon", "Leaving Soon", and "Left For Today"). These markers are also used in location's detail view. There's also an arrow in the top right that switches the visiting chef screen from today's visiting chefs to tomorrow's, so you can scout out what you might want to get tomorrow.
Locations are also now sorted alphabetically on the main menu, to make finding the location you're looking for easier.
2025-09-11 16:27:42 -04:00

255 lines
11 KiB
Swift

//
// DetailView.swift
// RIT Dining
//
// Created by Campbell on 9/1/25.
//
import SwiftUI
import SafariServices
struct DetailView: View {
@State var location: DiningLocation
@State private var isLoading: Bool = true
@State private var rotationDegrees: Double = 0
@State private var showingSafari: Bool = false
@State private var openString: String = ""
@State private var week: [Date] = []
@State private var weeklyHours: [[String]] = []
private let daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
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
}()
private func requestDone(result: Result<DiningLocationParser, Error>) -> Void {
switch result {
case .success(let location):
let diningInfo = parseLocationInfo(location: location)
if let times = diningInfo.diningTimes, !times.isEmpty {
var timeStrings: [String] = []
for time in times {
timeStrings.append("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
}
weeklyHours.append(timeStrings)
} else {
weeklyHours.append(["Closed"])
}
case .failure(let error):
print(error)
}
if week.count > 0 {
DispatchQueue.global().async {
let date_string = week.removeFirst().formatted(.iso8601
.year().month().day()
.dateSeparator(.dash))
getSingleDiningInfo(date: date_string, locationId: location.id, completionHandler: requestDone)
}
} else {
isLoading = false
print(weeklyHours)
}
}
private func getWeeklyHours() {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let dayOfWeek = calendar.component(.weekday, from: today)
week = calendar.range(of: .weekday, in: .weekOfYear, for: today)!
.compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: today) }
DispatchQueue.global().async {
let date_string = week.removeFirst().formatted(.iso8601
.year().month().day()
.dateSeparator(.dash))
getSingleDiningInfo(date: date_string, locationId: location.id, completionHandler: requestDone)
}
}
var body: some View {
if isLoading {
VStack {
Image(systemName: "fork.knife.circle")
.resizable()
.frame(width: 75, height: 75)
.foregroundStyle(.accent)
.rotationEffect(.degrees(rotationDegrees))
.onAppear {
withAnimation(animation) {
rotationDegrees = 360.0
}
}
Text("Loading...")
.foregroundStyle(.secondary)
}
.onAppear {
getWeeklyHours()
}
.padding()
} else {
ScrollView {
VStack(alignment: .leading) {
HStack(alignment: .center) {
Text(location.name)
.font(.title)
.fontWeight(.bold)
Spacer()
Button(action: {
showingSafari = true
}) {
Image(systemName: "map")
.foregroundStyle(.accent)
.font(.title3)
}
}
Text(location.summary)
.font(.title2)
.foregroundStyle(.secondary)
HStack(alignment: .top, spacing: 3) {
switch location.open {
case .open:
Text("Open")
.foregroundStyle(.green)
case .closed:
Text("Closed")
.foregroundStyle(.red)
case .openingSoon:
Text("Opening Soon")
.foregroundStyle(.orange)
case .closingSoon:
Text("Closing Soon")
.foregroundStyle(.orange)
}
Text("")
.foregroundStyle(.secondary)
VStack {
if let times = location.diningTimes, !times.isEmpty {
Text(openString)
.foregroundStyle(.secondary)
.onAppear {
openString = ""
for time in times {
openString += "\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime)), "
}
openString = String(openString.prefix(openString.count - 2))
}
} else {
Text("Not Open Today")
.foregroundStyle(.secondary)
}
}
}
.padding(.bottom, 12)
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
VStack(alignment: .leading) {
Text("Today's Visiting Chefs")
.font(.title3)
.fontWeight(.semibold)
ForEach(visitingChefs, id: \.self) { chef in
HStack(alignment: .top) {
Text(chef.name)
Spacer()
VStack(alignment: .trailing) {
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)
}
Text("\(display.string(from: chef.openTime)) - \(display.string(from: chef.closeTime))")
.foregroundStyle(.secondary)
}
}
Divider()
}
}
.padding(.bottom, 12)
}
if let dailySpecials = location.dailySpecials, !dailySpecials.isEmpty {
VStack(alignment: .leading) {
Text("Today's Daily Specials")
.font(.title3)
.fontWeight(.semibold)
ForEach(dailySpecials, id: \.self) { special in
HStack(alignment: .top) {
Text(special.name)
Spacer()
Text(special.type)
.foregroundStyle(.secondary)
}
Divider()
}
}
.padding(.bottom, 12)
}
VStack(alignment: .leading) {
Text("This Week's Hours")
.font(.title3)
.fontWeight(.semibold)
ForEach(weeklyHours.indices, id: \.self) { index in
HStack(alignment: .top) {
Text("\(daysOfWeek[index])")
Spacer()
VStack {
ForEach(weeklyHours[index].indices, id: \.self) { innerIndex in
Text(weeklyHours[index][innerIndex])
.foregroundStyle(.secondary)
}
}
}
Divider()
}
}
.padding(.bottom, 12)
// Ideally I'd like this text to be justified to more effectively use the screen space.
Text(location.desc)
.font(.body)
.padding(.bottom, 10)
Text("IMPORTANT: Some locations' descriptions may refer to them as being cashless during certain hours. This is outdated information, as all RIT Dining locations are now cashless 24/7.")
.font(.callout)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 8)
}
.navigationTitle("Details")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showingSafari) {
SafariView(url: URL(string: location.mapsUrl)!)
}
}
}
}
#Preview {
DetailView(location: DiningLocation(
id: 0,
name: "Example",
summary: "A Place",
desc: "A long description of the place",
mapsUrl: "https://example.com",
diningTimes: [DiningTimes(openTime: Date(), closeTime: Date())],
open: .open,
visitingChefs: nil,
dailySpecials: nil))
}