mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2026-03-03 12:45:28 -05:00
This increased padding is roughly the same as the padding around native iOS components like lists, which makes it look a lot more uniform. The increased padding also makes everything feel a little less cramped.
278 lines
12 KiB
Swift
278 lines
12 KiB
Swift
//
|
|
// DetailView.swift
|
|
// TigerDine
|
|
//
|
|
// Created by Campbell on 9/1/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SafariServices
|
|
|
|
struct DetailView: View {
|
|
@State var locationId: Int
|
|
|
|
@Environment(DiningModel.self) var model
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
@State private var showingSafari: Bool = false
|
|
@State private var occupancyLoading: Bool = true
|
|
@State private var occupancyPercentage: Double = 0.0
|
|
|
|
// This gets the location that we're meant to be displaying details about using the provided ID.
|
|
private var location: DiningLocation {
|
|
return model.locationsByDay[0].first { $0.id == locationId }!
|
|
}
|
|
|
|
// This creates a list of the time strings for the current day and following 6 days to display in the "Upcoming Hours" section.
|
|
// I realized that it makes a lot more sense to do today + 6 rather than just the current calendar week's hours, because who
|
|
// cares what Tuesday's hours were on Saturday, you want to know what Sunday's hours will be.
|
|
private var weeklyHours: [WeeklyHours] {
|
|
var newWeeklyHours: [WeeklyHours] = []
|
|
for day in model.locationsByDay {
|
|
for location in day {
|
|
if location.id == locationId {
|
|
let weekdayFormatter = DateFormatter()
|
|
weekdayFormatter.dateFormat = "EEEE"
|
|
if let times = location.diningTimes, !times.isEmpty {
|
|
var timeStrings: [String] = []
|
|
for time in times {
|
|
timeStrings.append("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
}
|
|
newWeeklyHours.append(
|
|
WeeklyHours(
|
|
day: weekdayFormatter.string(from: location.date),
|
|
date: location.date,
|
|
timeStrings: timeStrings
|
|
))
|
|
} else {
|
|
newWeeklyHours.append(
|
|
WeeklyHours(
|
|
day: weekdayFormatter.string(from: location.date),
|
|
date: location.date,
|
|
timeStrings: ["Closed"]
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newWeeklyHours
|
|
}
|
|
|
|
// Still a little broken, does not work for refresh. Need to fix.
|
|
private func getOccupancy() async {
|
|
// Only fetch occupancy data if the location is actually open right now. Otherwise, just exit early and hide the spinner.
|
|
if location.open == .open || location.open == .closingSoon {
|
|
occupancyLoading = true
|
|
switch await getOccupancyPercentage(mdoId: location.mdoId) {
|
|
case .success(let occupancy):
|
|
occupancyPercentage = occupancy
|
|
occupancyLoading = false
|
|
case .failure(let error):
|
|
print(error)
|
|
occupancyLoading = false
|
|
}
|
|
} else {
|
|
occupancyLoading = false
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading) {
|
|
VStack(alignment: .leading) {
|
|
Text(location.name)
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
Text(location.summary)
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(.secondary)
|
|
VStack(alignment: .leading) {
|
|
switch location.open {
|
|
case .open:
|
|
Text("Open")
|
|
.font(.title3)
|
|
.foregroundStyle(.green)
|
|
case .closed:
|
|
Text("Closed")
|
|
.font(.title3)
|
|
.foregroundStyle(.red)
|
|
case .openingSoon:
|
|
Text("Opening Soon")
|
|
.font(.title3)
|
|
.foregroundStyle(.orange)
|
|
case .closingSoon:
|
|
Text("Closing Soon")
|
|
.font(.title3)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
if let times = location.diningTimes, !times.isEmpty {
|
|
ForEach(times, id: \.self) { time in
|
|
Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
Text("Not Open Today")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
#if DEBUG
|
|
HStack(spacing: 0) {
|
|
ForEach(Range(1...5), id: \.self) { index in
|
|
if occupancyPercentage > (20 * Double(index)) {
|
|
Image(systemName: "person.fill")
|
|
} else {
|
|
Image(systemName: "person")
|
|
}
|
|
}
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.frame(width: 18, height: 18)
|
|
.opacity(occupancyLoading ? 1 : 0)
|
|
.task {
|
|
await getOccupancy()
|
|
}
|
|
}
|
|
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
|
|
.font(.title3)
|
|
#endif
|
|
}
|
|
.padding(.bottom, 16)
|
|
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("\(dateDisplay.string(from: chef.openTime)) - \(dateDisplay.string(from: chef.closeTime))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
.padding(.bottom, 16)
|
|
}
|
|
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, 16)
|
|
}
|
|
VStack(alignment: .leading) {
|
|
Text("Upcoming Hours")
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
ForEach(weeklyHours, id: \.self) { day in
|
|
HStack(alignment: .top) {
|
|
Text(day.day)
|
|
Spacer()
|
|
VStack {
|
|
ForEach(day.timeStrings, id: \.self) { timeString in
|
|
Text(timeString)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
.padding(.bottom, 16)
|
|
// Ideally I'd like this text to be justified to more effectively use the screen space.
|
|
Text(location.desc)
|
|
.font(.body)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
// Favorites toggle button.
|
|
Button(action: {
|
|
if model.favorites.contains(location) {
|
|
model.favorites.remove(location)
|
|
} else {
|
|
model.favorites.add(location)
|
|
}
|
|
}) {
|
|
if model.favorites.contains(location) {
|
|
Image(systemName: "star.fill")
|
|
.foregroundStyle(.yellow)
|
|
//.font(.title3)
|
|
} else {
|
|
Image(systemName: "star")
|
|
.foregroundStyle(.yellow)
|
|
//.font(.title3)
|
|
}
|
|
}
|
|
// THIS FEATURE DISABLED AT RIT'S REQUEST FOR SECURITY REASONS.
|
|
// No hard feelings or anything though, I get it.
|
|
// // Open OnDemand. Unfortunately the locations use arbitrary IDs, so just open the main OnDemand page.
|
|
// Button(action: {
|
|
// openURL(URL(string: "https://ondemand.rit.edu")!)
|
|
// }) {
|
|
// Image(systemName: "cart")
|
|
// .font(.title3)
|
|
// }
|
|
// .disabled(location.open == .closed || location.open == .openingSoon)
|
|
// Open this location on the RIT map in embedded Safari.
|
|
Button(action: {
|
|
showingSafari = true
|
|
}) {
|
|
Image(systemName: "map")
|
|
//.font(.title3)
|
|
}
|
|
if let fdmpIds = location.fdmpIds {
|
|
NavigationLink(destination: MenuView(accountId: fdmpIds.accountId, locationId: fdmpIds.locationId)) {
|
|
Image(systemName: "menucard")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Details")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.sheet(isPresented: $showingSafari) {
|
|
SafariView(url: URL(string: location.mapsUrl)!)
|
|
}
|
|
.refreshable {
|
|
do {
|
|
try await model.getHoursByDay()
|
|
} catch {
|
|
print(error)
|
|
}
|
|
await getOccupancy()
|
|
}
|
|
}
|
|
}
|