mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2025-10-19 06:36:18 -04:00
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.
255 lines
11 KiB
Swift
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))
|
|
}
|