Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions VITTY/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Google Services configuration file
GoogleService-Info.plist
4 changes: 4 additions & 0 deletions VITTY/VITTY.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
4B183EEA2D7C793800C9D801 /* RemindersData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B183EE92D7C791400C9D801 /* RemindersData.swift */; };
4B183EEC2D7CB15800C9D801 /* CourseRefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B183EEB2D7CB11500C9D801 /* CourseRefs.swift */; };
4B1BDBCC2E1396B1008C2DE9 /* ToolTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1BDBCB2E1396A9008C2DE9 /* ToolTip.swift */; };
4B2D1F0F2E26060C002AFD25 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B2D1F0E2E26060C002AFD25 /* GoogleService-Info.plist */; };
4B2D648F2E20BA6300412CB7 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D648E2E20BA5A00412CB7 /* NetworkMonitor.swift */; };
4B2D64902E20BA6300412CB7 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D648E2E20BA5A00412CB7 /* NetworkMonitor.swift */; };
4B2D64922E20C1AC00412CB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B2D64912E20C1AC00412CB7 /* GoogleService-Info.plist */; };
Expand Down Expand Up @@ -192,6 +193,7 @@
4B183EE92D7C791400C9D801 /* RemindersData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersData.swift; sourceTree = "<group>"; };
4B183EEB2D7CB11500C9D801 /* CourseRefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRefs.swift; sourceTree = "<group>"; };
4B1BDBCB2E1396A9008C2DE9 /* ToolTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTip.swift; sourceTree = "<group>"; };
4B2D1F0E2E26060C002AFD25 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
4B2D648E2E20BA5A00412CB7 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
4B2D64912E20C1AC00412CB7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
4B2DD6942E0A702D00BC3B67 /* CircleRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleRequests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -406,6 +408,7 @@
4BC853C52DF6F71B0092B2E2 /* VittyWidgetExtension.entitlements */,
4B2D64912E20C1AC00412CB7 /* GoogleService-Info.plist */,
52EE849D2CB9CD1F00CD864C /* GoogleService-Info.plist */,
4B2D1F0E2E26060C002AFD25 /* GoogleService-Info.plist */,
5251A7FF2B46E3C000D44CFE /* .swift-format */,
314A408E27383BEC0058082F /* VITTYApp.swift */,
314A409027383BEC0058082F /* ContentView.swift */,
Expand Down Expand Up @@ -1097,6 +1100,7 @@
31128CFE2772F57E0084C9EA /* Poppins-MediumItalic.ttf in Resources */,
31128CFD2772F57E0084C9EA /* Poppins-SemiBold.ttf in Resources */,
31128CF92772F57E0084C9EA /* Poppins-Medium.ttf in Resources */,
4B2D1F0F2E26060C002AFD25 /* GoogleService-Info.plist in Resources */,
31128CFA2772F57E0084C9EA /* Poppins-SemiBoldItalic.ttf in Resources */,
31128CFC2772F57E0084C9EA /* Poppins-Regular.ttf in Resources */,
52EE849E2CB9CD1F00CD864C /* GoogleService-Info.plist in Resources */,
Expand Down
230 changes: 163 additions & 67 deletions VITTY/VITTY/EmptyClassroom/View/EmptyClass.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
//
// EmptyClassAPIService.swift
// VITTY
//
// Created by Rujin Devkota on 2/27/25.
//

import SwiftUI

struct EmptyClassRoom: View {
Expand Down Expand Up @@ -88,83 +95,172 @@ struct EmptyClassRoom: View {
private var contentView: some View {
Group {
if viewModel.isLoading {
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.2)
.tint(.white)
Text("Loading classrooms...")
.foregroundColor(.white.opacity(0.8))
.font(.subheadline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
loadingView
} else if viewModel.isGenerating {
generatingView
} else if let errorMessage = viewModel.errorMessage {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 40))
.foregroundColor(.red.opacity(0.8))
Text("Error")
.font(.headline)
.foregroundColor(.white)
Text(errorMessage)
.foregroundColor(.red.opacity(0.8))
.multilineTextAlignment(.center)
.font(.subheadline)
Button("Retry") {
Task {
await viewModel.fetchEmptyClassrooms(slot: selectedSlot, authToken: authViewModel.loggedInBackendUser?.token ?? "")
}
}
errorView(errorMessage)
} else if viewModel.emptyClassrooms.isEmpty {
emptyStateView
} else if filteredClassrooms.isEmpty && !searchText.isEmpty {
noResultsView
} else {
classroomsGrid
}
}
}

private var loadingView: some View {
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.2)
.tint(.white)
Text("Loading classrooms...")
.foregroundColor(.white.opacity(0.8))
.font(.subheadline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}

private var generatingView: some View {
VStack(spacing: 24) {
// Animated hourglass icon
Image(systemName: "hourglass")
.font(.system(size: 50))
.foregroundColor(.blue.opacity(0.7))
.rotationEffect(.degrees(viewModel.isGenerating ? 180 : 0))
.animation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: viewModel.isGenerating)

VStack(spacing: 16) {
Text("Preparing Your Data")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.white)

Text("Our system is currently generating the latest classroom information for slot \(selectedSlot).")
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.font(.body)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.7))
.cornerRadius(8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else if viewModel.emptyClassrooms.isEmpty {
VStack(spacing: 16) {
Image(systemName: "building.2")
.font(.system(size: 40))

VStack(spacing: 8) {
Text("This process may take a few moments")
.foregroundColor(.blue.opacity(0.8))
.font(.subheadline)

Text("Please try reloading in a moment")
.foregroundColor(.white.opacity(0.6))
Text("No Classrooms Available")
.font(.headline)
.foregroundColor(.white)
Text("There are no empty classrooms for slot \(selectedSlot) at this time.")
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.font(.caption)
}
.padding(.top, 8)
}

Button(action: {
viewModel.reload(slot: selectedSlot, authToken: authViewModel.loggedInBackendUser?.token ?? "")
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14, weight: .medium))
Text("Reload")
.font(.subheadline)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else if filteredClassrooms.isEmpty && !searchText.isEmpty {
VStack(spacing: 16) {
Image(systemName: "magnifyingglass")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("No Results Found")
.font(.headline)
.foregroundColor(.white)
Text("No classrooms match '\(searchText)'")
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue.opacity(0.7))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
)
}
.padding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}

private func errorView(_ errorMessage: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 40))
.foregroundColor(.orange.opacity(0.8))

Text("Oops!")
.font(.headline)
.foregroundColor(.white)

Text(errorMessage)
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.font(.subheadline)
.padding(.horizontal)

Button(action: {
viewModel.reload(slot: selectedSlot, authToken: authViewModel.loggedInBackendUser?.token ?? "")
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14, weight: .medium))
Text("Reload")
.font(.subheadline)
Button("Clear Search") {
searchText = ""
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.7))
.cornerRadius(8)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else {
classroomsGrid
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.orange.opacity(0.7))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}

private var emptyStateView: some View {
VStack(spacing: 16) {
Image(systemName: "building.2")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("No Classrooms Available")
.font(.headline)
.foregroundColor(.white)
Text("There are no empty classrooms for slot \(selectedSlot) at this time.")
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.font(.subheadline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}

private var noResultsView: some View {
VStack(spacing: 16) {
Image(systemName: "magnifyingglass")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("No Results Found")
.font(.headline)
.foregroundColor(.white)
Text("No classrooms match '\(searchText)'")
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.font(.subheadline)
Button("Clear Search") {
searchText = ""
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.7))
.cornerRadius(8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}

private var classroomsGrid: some View {
Expand Down
33 changes: 31 additions & 2 deletions VITTY/VITTY/EmptyClassroom/ViewModel/EmptyClassRoomViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,52 @@ class EmptyClassroomViewModel: ObservableObject {
@Published var emptyClassrooms: [String] = []
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isGenerating: Bool = false
private var currentSlot: String = "A1"

func fetchEmptyClassrooms(slot: String, authToken: String) async {
guard !slot.isEmpty else { return }

isLoading = true
errorMessage = nil
isGenerating = false
currentSlot = slot

do {
let classrooms = try await EmptyClassRoomAPIService.shared.getEmptyClassrooms(slot: slot, authToken: authToken)
emptyClassrooms = classrooms
isGenerating = false
} catch {
errorMessage = "Failed to load empty classrooms: \(error.localizedDescription)"
handleError(error)
}

isLoading = false
}

private func handleError(_ error: Error) {
let errorDescription = error.localizedDescription.lowercased()

// Check if this is a server error (likely generation in progress)
if let nsError = error as NSError?,
nsError.code >= 500 ||
errorDescription.contains("contact vitty support") ||
errorDescription.contains("failed to parse error response") ||
errorDescription.contains("generating") ||
errorDescription.contains("server error") {

// Show generation state instead of error
isGenerating = true
errorMessage = nil
} else {
// Only show actual errors for client-side issues
isGenerating = false
errorMessage = "Failed to load empty classrooms: \(error.localizedDescription)"
}
}

func reload(slot: String, authToken: String) {
Task {
await fetchEmptyClassrooms(slot: slot, authToken: authToken)
}
}
}

Loading