Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions berkeley-mobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
01CDBBF125CA6F58006B93BD /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CDBBF025CA6F58006B93BD /* RequestError.swift */; };
01CDFF6A257C614900D9FBD6 /* Colors+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CDFF69257C614900D9FBD6 /* Colors+Resource.swift */; };
01D11B8E2504453B00BDF660 /* ScrollingStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D11B8D2504453B00BDF660 /* ScrollingStackView.swift */; };
01D11B902504560700BDF660 /* GymDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D11B8F2504560700BDF660 /* GymDetailViewController.swift */; };
01D269932544D86C000377B4 /* Apercu Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698A2544D86B000377B4 /* Apercu Light.otf */; };
01D269942544D86C000377B4 /* Apercu Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698B2544D86B000377B4 /* Apercu Italic.otf */; };
01D269952544D86C000377B4 /* Apercu Light Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698C2544D86B000377B4 /* Apercu Light Italic.otf */; };
Expand Down Expand Up @@ -79,6 +78,7 @@
13EA64CA2399CE5B00FD8E13 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64C92399CE5B00FD8E13 /* SearchItem.swift */; };
13EA64CD2399CEDA00FD8E13 /* Gym.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CC2399CEDA00FD8E13 /* Gym.swift */; };
13EA64D02399D50C00FD8E13 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CF2399D50C00FD8E13 /* DataManager.swift */; };
1D7DD6E02DC2EB5B00A6BBA7 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */; };
1DB006AD2D71C8D6001CC870 /* ResourcesSectionDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */; };
1DB88F6D2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */; };
29061D41241C450E002BC9D9 /* HasLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29061D40241C450E002BC9D9 /* HasLocation.swift */; };
Expand Down Expand Up @@ -217,7 +217,6 @@
01CDBBF025CA6F58006B93BD /* RequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = "<group>"; };
01CDFF69257C614900D9FBD6 /* Colors+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Resource.swift"; sourceTree = "<group>"; };
01D11B8D2504453B00BDF660 /* ScrollingStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingStackView.swift; sourceTree = "<group>"; };
01D11B8F2504560700BDF660 /* GymDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailViewController.swift; sourceTree = "<group>"; };
01D2698A2544D86B000377B4 /* Apercu Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Light.otf"; sourceTree = "<group>"; };
01D2698B2544D86B000377B4 /* Apercu Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Italic.otf"; sourceTree = "<group>"; };
01D2698C2544D86B000377B4 /* Apercu Light Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Light Italic.otf"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -271,6 +270,7 @@
13EA64C92399CE5B00FD8E13 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = "<group>"; };
13EA64CC2399CEDA00FD8E13 /* Gym.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gym.swift; sourceTree = "<group>"; };
13EA64CF2399D50C00FD8E13 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = "<group>"; };
1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesSectionDropdown.swift; sourceTree = "<group>"; };
1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardSwiftUIView.swift; sourceTree = "<group>"; };
29061D40241C450E002BC9D9 /* HasLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasLocation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -521,12 +521,12 @@
children = (
13EA64C62399CDF900FD8E13 /* GymDataSource */,
1336A31D241C400F00949F32 /* GymClassDataSource */,
01D11B8F2504560700BDF660 /* GymDetailViewController.swift */,
13E25DFC238C949E00B670B5 /* FitnessViewController.swift */,
135D7F78243AA6B1003F8BD1 /* Fitness+Controllers */,
E83B6DA42D7A85D500AA9422 /* GymOccupancyScrapper.swift */,
E83B6DA62D7A85F200AA9422 /* GymOccupancyView.swift */,
E83B6DA82D7A860E00AA9422 /* GymOccupancyViewModel.swift */,
1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */,
);
path = Fitness;
sourceTree = "<group>";
Expand Down Expand Up @@ -1187,6 +1187,7 @@
01D2699D2544E005000377B4 /* AcademicCalendarViewController.swift in Sources */,
1336A31C241C400800949F32 /* GymClass.swift in Sources */,
E8B5975F2CA62D6F006DFBD5 /* SegmentedControlView.swift in Sources */,
1D7DD6E02DC2EB5B00A6BBA7 /* GymDetailView.swift in Sources */,
13EA64C82399CE0800FD8E13 /* GymDataSource.swift in Sources */,
2E1C227D2D835A9D0021803C /* SearchBarView.swift in Sources */,
298EE26A25BB6C33002BAF0F /* Colors+StudyPact.swift in Sources */,
Expand Down Expand Up @@ -1235,7 +1236,6 @@
017C0B26251018BA00BFA80A /* Colors+MapMarker.swift in Sources */,
E83B6DA92D7A860E00AA9422 /* GymOccupancyViewModel.swift in Sources */,
29345E2724A7E76300859A88 /* OverviewCardView.swift in Sources */,
01D11B902504560700BDF660 /* GymDetailViewController.swift in Sources */,
55AF442D2453ACE600F13232 /* DiningLocation.swift in Sources */,
1336A329241DA56100949F32 /* DiningHallDataSource.swift in Sources */,
136DC97B2398B4D1009B1810 /* UIViewController+Extensions.swift in Sources */,
Expand Down
15 changes: 13 additions & 2 deletions berkeley-mobile/Fitness/GymDataSource/Gym.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,20 @@ class Gym: SearchItem, HasLocation, CanFavorite, HasPhoneNumber, HasImage, HasOp
self.phoneNumber = phoneNumber
self.weeklyHours = weeklyHours
self.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
self.imageURL = URL(string: imageLink ?? "")

if let imgLink = imageLink, !imgLink.isEmpty, let url = URL(string: imgLink) {
self.imageURL = url
} else {
self.imageURL = nil
}

self.icon = UIImage(named: "Walk")?.colored(BMColor.blackText)
self.website = URL(string: link ?? "")

if let webLink = link, !webLink.isEmpty, let url = URL(string: webLink) {
self.website = url
} else {
self.website = nil
}
}

}
Expand Down
209 changes: 209 additions & 0 deletions berkeley-mobile/Fitness/GymDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//
// GymDetailView.swift
//
// Created by Yihang Chen on 4/9/25.
// Copyright © 2025 ASUC OCTO. All rights reserved.
//

import SwiftUI

struct CategoryOverviewCard: View {
let category: SearchItem & HasLocation & HasImage

var body: some View {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading) {
TitleView(name: category.name)

Spacer(minLength: 20)

ContactInfoView(category: category)
}

Spacer()

ImageView(imageURL: category.imageURL)
}
.padding(12)
.background(Color(BMColor.cardBackground))
.cornerRadius(12)
.shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0)
.padding(.vertical, 8)
.padding(.horizontal, 4)
}

struct TitleView: View {
let name: String

var body: some View {
Text(name)
.font(Font(BMFont.bold(23)))
.foregroundColor(Color(BMColor.blackText))
.lineLimit(3)
.padding(.top, 8)
}
}

struct ContactInfoView: View {
let category: SearchItem & HasLocation

var body: some View {
VStack(alignment: .leading, spacing: 12) {
if let address = category.address, !address.isEmpty {
ContactInfoRow(
iconName: "location.fill",
text: address,
lineLimit: nil
)
}

if let hasPhone = category as? HasPhoneNumber,
let phoneNumber = hasPhone.phoneNumber,
!phoneNumber.isEmpty {
ContactInfoRow(
iconName: "phone.fill",
text: phoneNumber
)
}

if let distance = category.distanceToUser {
ContactInfoRow(
iconName: "figure.walk",
text: String(format: "%.1f miles", distance)
)
}
}
.foregroundColor(Color(BMColor.blackText))
}
}

struct ContactInfoRow: View {
let iconName: String
let text: String
var lineLimit: Int? = 1

var body: some View {
HStack(alignment: .top, spacing: 10) {
Image(systemName: iconName)
.frame(width: 18, height: 18)

Text(text)
.font(Font(BMFont.light(12)))
.lineLimit(lineLimit)
.fixedSize(horizontal: false, vertical: true)
}
}
}

struct ImageView: View {
let imageURL: URL?

var body: some View {
if let imageURL = imageURL {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
defaultImage
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 120, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(uiColor: .systemGray4), lineWidth: 0.5)
)
case .failure:
defaultImage
@unknown default:
EmptyView()
}
}
} else {
defaultImage
}
}

private var defaultImage: some View {
Image(uiImage: Constants.doeGladeImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 120, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(uiColor: .systemGray4), lineWidth: 0.5)
)
}
}
}

struct GymDetailView: View {
@Environment(\.openURL) private var openURL

let gym: Gym
init(gym: Gym) {
self.gym = gym
}

var body: some View {
ScrollView {
VStack(spacing: 16) {
CategoryOverviewCard(category: gym)

if gym.weeklyHours != nil {
OpenTimesCardSwiftUIView(item: gym)
}

if let website = gym.website {
BMActionButton(title: "Learn More") {
openURL(website)
}
}

if let description = gym.description, !description.isEmpty {
descriptionCard(description: description)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 5)
}
.navigationTitle(gym.name)
}

private func descriptionCard(description: String) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text("Description")
.font(Font(BMFont.bold(16)))
.foregroundColor(Color(BMColor.blackText))

Text(description)
.font(Font(BMFont.light(12)))
.foregroundColor(Color(BMColor.blackText))
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(BMColor.cardBackground))
.cornerRadius(12)
}
}

#Preview {
let sampleGym = Gym(
name: "RSF (Recreational Sports Facility)",
description: "The Recreational Sports Facility (RSF) is UC Berkeley's largest fitness center, offering state-of-the-art equipment, group exercise classes, and various sports courts. Located at the heart of campus, it provides comprehensive fitness options for students and faculty.",
address: "2301 Bancroft Way, Berkeley, CA 94720",
phoneNumber: "(510) 111-2222",
imageLink: nil,
weeklyHours: nil,
link: "https://recsports.berkeley.edu/rsf/"
)

sampleGym.latitude = 37.8687
sampleGym.longitude = -122.2614

return NavigationView {
GymDetailView(gym: sampleGym)
}
}
Loading