mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2025-10-19 06:36:18 -04:00
Improved DetailView, preliminary visiting chef support
The DetailView now presents information in a more appealing way, and also fetches the opening hours for the entire week, so you can see more than just the current day's hours for a location. Also added preliminary support for parsing visiting chef information. Times are not being parsed yet because the formatting for them is super bad and inconsistent, but the names and descriptions are parsed. A "Today's Visiting Chefs" button has been added to the top of ContentView that brings you to a basic screen listing all of the locations with visiting chefs and telling you what they are. Currently times are presented as part of the name of the location like they are in the TigerCenter response data.
This commit is contained in:
parent
30c025e113
commit
ea2538ce18
@ -254,7 +254,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -283,7 +283,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
@ -70,12 +70,12 @@ struct ContentView: View {
|
||||
// Asynchronously fetch the data for all of the locations and parse their data to display it.
|
||||
private func getDiningData() {
|
||||
var newDiningLocations: [DiningLocation] = []
|
||||
getAllDiningInfo { result in
|
||||
getAllDiningInfo(date: nil) { result in
|
||||
DispatchQueue.global().async {
|
||||
switch result {
|
||||
case .success(let locations):
|
||||
for i in 0..<locations.locations.count {
|
||||
let diningInfo = getLocationInfo(location: locations.locations[i])
|
||||
let diningInfo = parseLocationInfo(location: locations.locations[i])
|
||||
print(diningInfo.name)
|
||||
DispatchQueue.global().sync {
|
||||
newDiningLocations.append(diningInfo)
|
||||
@ -123,6 +123,13 @@ struct ContentView: View {
|
||||
} else {
|
||||
VStack() {
|
||||
List {
|
||||
if searchText.isEmpty {
|
||||
Section(content: {
|
||||
NavigationLink(destination: VisitingChefs(diningLocations: diningLocations)) {
|
||||
Text("Today's Visiting Chefs")
|
||||
}
|
||||
})
|
||||
}
|
||||
Section(content: {
|
||||
LocationList(diningLocations: filteredLocations)
|
||||
}, footer: {
|
||||
|
@ -23,7 +23,19 @@ struct SafariView: UIViewControllerRepresentable {
|
||||
|
||||
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()
|
||||
@ -33,15 +45,79 @@ struct DetailView: View {
|
||||
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) {
|
||||
Text(location.name)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Text(location.summary)
|
||||
.font(.title2)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .top) {
|
||||
HStack(alignment: .top, spacing: 3) {
|
||||
switch location.open {
|
||||
case .open:
|
||||
Text("Open")
|
||||
@ -56,11 +132,18 @@ struct DetailView: View {
|
||||
Text("Closing Soon")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
Text("•")
|
||||
.foregroundStyle(.secondary)
|
||||
VStack {
|
||||
if let times = location.diningTimes, !times.isEmpty {
|
||||
ForEach(times, id: \.self) { time in
|
||||
Text("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
|
||||
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")
|
||||
@ -75,6 +158,25 @@ struct DetailView: View {
|
||||
Text("View on Map")
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
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, 10)
|
||||
Text(location.desc)
|
||||
.font(.body)
|
||||
.padding(.bottom, 10)
|
||||
@ -90,6 +192,7 @@ struct DetailView: View {
|
||||
SafariView(url: URL(string: location.mapsUrl)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@ -100,5 +203,6 @@ struct DetailView: View {
|
||||
desc: "A long description of the place",
|
||||
mapsUrl: "https://example.com",
|
||||
diningTimes: [DiningTimes(openTime: Date(), closeTime: Date())],
|
||||
open: .open))
|
||||
open: .open,
|
||||
visitingChefs: nil))
|
||||
}
|
||||
|
@ -11,13 +11,17 @@ enum InvalidHTTPError: Error {
|
||||
case invalid
|
||||
}
|
||||
|
||||
// This code came from another project of mine and was used to fetch the GitHub API for update checking. I just copied it here, but it can
|
||||
// probably be made simpler for this use case.
|
||||
func getAllDiningInfo(completionHandler: @escaping (Result<DiningLocationsParser, Error>) -> Void) {
|
||||
// This API requesting code came from another project of mine and was used to fetch the GitHub API for update checking. I just copied it
|
||||
// here, but it can probably be made simpler for this use case.
|
||||
|
||||
// Get information for all dining locations.
|
||||
func getAllDiningInfo(date: String?, completionHandler: @escaping (Result<DiningLocationsParser, Error>) -> Void) {
|
||||
// The endpoint requires that you specify a date, so get today's.
|
||||
let date_string = Date().formatted(.iso8601
|
||||
let date_string: String = if let date { date } else {
|
||||
Date().formatted(.iso8601
|
||||
.year().month().day()
|
||||
.dateSeparator(.dash))
|
||||
}
|
||||
let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(date_string)"
|
||||
|
||||
guard let url = URL(string: url_string) else {
|
||||
@ -44,7 +48,42 @@ func getAllDiningInfo(completionHandler: @escaping (Result<DiningLocationsParser
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
// Get information for just one dining location based on its location ID.
|
||||
func getSingleDiningInfo(date: String?, locationId: Int, completionHandler: @escaping (Result<DiningLocationParser, Error>) -> Void) {
|
||||
// The current date and the location ID are required to get information for just one location.
|
||||
let date_string: String = if let date { date } else {
|
||||
Date().formatted(.iso8601
|
||||
.year().month().day()
|
||||
.dateSeparator(.dash))
|
||||
}
|
||||
let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-single?date=\(date_string)&locId=\(locationId)"
|
||||
print("making request to \(url_string)")
|
||||
|
||||
guard let url = URL(string: url_string) else {
|
||||
print("Invalid URL")
|
||||
return
|
||||
}
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
guard case .none = error else { return }
|
||||
|
||||
guard let data = data else {
|
||||
print("Data error.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
||||
completionHandler(.failure(InvalidHTTPError.invalid))
|
||||
return
|
||||
}
|
||||
|
||||
let decoded: Result<DiningLocationParser, Error> = Result(catching: { try JSONDecoder().decode(DiningLocationParser.self, from: data) })
|
||||
completionHandler(decoded)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func parseLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
print("beginning parse for \(location.name)")
|
||||
|
||||
// The descriptions sometimes have HTML <br /> tags despite also having \n. Those need to be removed.
|
||||
@ -59,7 +98,8 @@ func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
desc: desc,
|
||||
mapsUrl: location.mapsUrl,
|
||||
diningTimes: nil,
|
||||
open: .closed)
|
||||
open: .closed,
|
||||
visitingChefs: nil)
|
||||
}
|
||||
|
||||
var openStrings: [String] = []
|
||||
@ -78,7 +118,8 @@ func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
desc: desc,
|
||||
mapsUrl: location.mapsUrl,
|
||||
diningTimes: nil,
|
||||
open: .closed)
|
||||
open: .closed,
|
||||
visitingChefs: nil)
|
||||
}
|
||||
openStrings.append(exceptions[0].startTime)
|
||||
closeStrings.append(exceptions[0].endTime)
|
||||
@ -153,6 +194,23 @@ func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the "menus" array and keep track of visiting chefs at this location, if there are any. If not then we can just save nil.
|
||||
// Eventually this will parse out the times, but that's complicated because that data is formatted poorly and inconsistently and
|
||||
// I'm not interested in messing with that quite yet.
|
||||
let visitingChefs: [VisitngChef]?
|
||||
if !location.menus.isEmpty {
|
||||
var chefs: [VisitngChef] = []
|
||||
for menu in location.menus {
|
||||
if menu.category == "Visiting Chef" {
|
||||
print("found visiting chef: \(menu.name)")
|
||||
chefs.append(VisitngChef(name: menu.name, description: menu.description!))
|
||||
}
|
||||
}
|
||||
visitingChefs = chefs
|
||||
} else {
|
||||
visitingChefs = nil
|
||||
}
|
||||
|
||||
return DiningLocation(
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
@ -160,5 +218,6 @@ func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
desc: desc,
|
||||
mapsUrl: location.mapsUrl,
|
||||
diningTimes: diningTimes,
|
||||
open: openStatus)
|
||||
open: openStatus,
|
||||
visitingChefs: visitingChefs)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import Foundation
|
||||
// be improved later when I feel like it.
|
||||
struct DiningLocationParser: Decodable {
|
||||
// An individual "event", which is just an open period for the location.
|
||||
struct Events: Decodable {
|
||||
struct Event: Decodable {
|
||||
// Hour exceptions for the given event.
|
||||
struct HoursException: Decodable {
|
||||
let id: Int
|
||||
@ -26,13 +26,21 @@ struct DiningLocationParser: Decodable {
|
||||
let endTime: String
|
||||
let exceptions: [HoursException]?
|
||||
}
|
||||
// An individual "menu", which can be either a daily special item or a visitng chef. Description needs to be optional because
|
||||
// visiting chefs have descriptions but specials do not.
|
||||
struct Menu: Decodable {
|
||||
let name: String
|
||||
let description: String?
|
||||
let category: String
|
||||
}
|
||||
// Other basic information to read from a location's JSON that we'll need later.
|
||||
let id: Int
|
||||
let name: String
|
||||
let summary: String
|
||||
let description: String
|
||||
let mapsUrl: String
|
||||
let events: [Events]
|
||||
let events: [Event]
|
||||
let menus: [Menu]
|
||||
}
|
||||
|
||||
// Struct that probably doesn't need to exist but this made parsing the list of location responses easy.
|
||||
@ -54,6 +62,12 @@ struct DiningTimes: Equatable, Hashable {
|
||||
var closeTime: Date
|
||||
}
|
||||
|
||||
// A visitng chef present at a location.
|
||||
struct VisitngChef: Equatable, Hashable {
|
||||
let name: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
// The basic information about a dining location needed to display it in the app after parsing is finished.
|
||||
struct DiningLocation: Identifiable, Hashable {
|
||||
let id: Int
|
||||
@ -63,4 +77,5 @@ struct DiningLocation: Identifiable, Hashable {
|
||||
let mapsUrl: String
|
||||
let diningTimes: [DiningTimes]?
|
||||
let open: OpenStatus
|
||||
let visitingChefs: [VisitngChef]?
|
||||
}
|
||||
|
47
RIT Dining/VisitingChefs.swift
Normal file
47
RIT Dining/VisitingChefs.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// VisitingChefs.swift
|
||||
// RIT Dining
|
||||
//
|
||||
// Created by Campbell on 9/8/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VisitingChefs: View {
|
||||
@State var diningLocations: [DiningLocation]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(diningLocations, id: \.self) { location in
|
||||
if let visitingChefs = location.visitingChefs, !visitingChefs.isEmpty {
|
||||
VStack {
|
||||
Text(location.name)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
ForEach(visitingChefs, id: \.self) { chef in
|
||||
Text(chef.name)
|
||||
.fontWeight(.semibold)
|
||||
Text(chef.description)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Visiting Chefs")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VisitingChefs(
|
||||
diningLocations: [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: [VisitngChef(name: "Example Chef (1-2 p.m.)", description: "Serves example food")])])
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user