From 1ca527d2fbc5284ee863c88abc118992aec6016b Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Mon, 3 Feb 2025 20:53:11 -0700 Subject: [PATCH 1/7] Fix the location filtering issue. --- FlatMate/ViewModel/SwipeViewModel.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/FlatMate/ViewModel/SwipeViewModel.swift b/FlatMate/ViewModel/SwipeViewModel.swift index a7c564b..5f07431 100644 --- a/FlatMate/ViewModel/SwipeViewModel.swift +++ b/FlatMate/ViewModel/SwipeViewModel.swift @@ -47,11 +47,14 @@ class SwipeViewModel: ObservableObject { await MainActor.run { self.isLoading = true } do { - // Step 1: Fetch the user's swiped profiles + // 1. Fetch the current user doc to grab its 'city' and swiped profiles let userDoc = try await db.collection("users").document(currentUserID).getDocument() let swipedProfiles = userDoc.data()?["swipedProfiles"] as? [String] ?? [] - // Step 2: Also fetch existing matches to exclude them + // 2. Also figure out the city of this user (default to empty if missing) + let currentUserCity = userDoc.data()?["city"] as? String ?? "" + + // 3. Also fetch existing matches so we can exclude them let matchesSnapshot = try await db.collection("matches").getDocuments() let matchedProfiles = matchesSnapshot.documents.compactMap { doc -> String? in let data = doc.data() @@ -66,18 +69,20 @@ class SwipeViewModel: ObservableObject { return nil } - // Step 3: Combine both sets of profiles to exclude + // 4. Combine both sets of profiles to exclude let excludedProfiles = Set(swipedProfiles + matchedProfiles) - // Step 4: Fetch and filter available profiles + // 5. Fetch other profiles *only* from the same city let snapshot = try await db.collection("users") + .whereField("city", isEqualTo: currentUserCity) .whereField("id", isNotEqualTo: currentUserID) .getDocuments() + // 6. Decode and filter out excluded profiles let profiles = snapshot.documents.compactMap { doc -> ProfileCardView.Model? in do { let profile = try doc.data(as: ProfileCardView.Model.self) - // Only include profiles that haven't been swiped or matched + // Exclude swiped or matched profiles return excludedProfiles.contains(profile.id) ? nil : profile } catch { print("Error decoding profile: \(error.localizedDescription)") @@ -94,4 +99,5 @@ class SwipeViewModel: ObservableObject { await MainActor.run { self.isLoading = false } } } + } From e0c139eb89b792bfb1c233c23f7cb91e3493b681 Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Mon, 3 Feb 2025 21:27:07 -0700 Subject: [PATCH 2/7] added the start again button in the swipe view --- FlatMate/View/SwipePageView.swift | 18 +++++++++++++++--- FlatMate/ViewModel/SwipeViewModel.swift | 24 ++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/FlatMate/View/SwipePageView.swift b/FlatMate/View/SwipePageView.swift index 4b905fe..0400672 100644 --- a/FlatMate/View/SwipePageView.swift +++ b/FlatMate/View/SwipePageView.swift @@ -32,9 +32,21 @@ struct SwipePageView: View { otherUser: lastMatch! ) } else if viewModel.profiles.isEmpty { - Text("No profiles available.") - .font(.custom("Outfit-Regular", size: 20)) - .foregroundColor(.gray) + VStack(spacing: 16) { + + + Button(action: { + Task { + await viewModel.resetSwipedProfiles(forUser: userID) + await viewModel.fetchProfiles(currentUserID: userID) + } + }) { + Text("Start Over") + .font(.custom("Outfit-Regular", size: 30)) + .foregroundColor(.gray) + } + + } } else { let swipeModel = SwipeCardsView.Model(cards: viewModel.profiles) SwipeCardsView(model: swipeModel) { model in diff --git a/FlatMate/ViewModel/SwipeViewModel.swift b/FlatMate/ViewModel/SwipeViewModel.swift index 5f07431..c437a87 100644 --- a/FlatMate/ViewModel/SwipeViewModel.swift +++ b/FlatMate/ViewModel/SwipeViewModel.swift @@ -42,19 +42,31 @@ class SwipeViewModel: ObservableObject { } } + func resetSwipedProfiles(forUser userID: String) async { + let db = Firestore.firestore() + do { + try await db.collection("users") + .document(userID) + .updateData(["swipedProfiles": FieldValue.delete()]) + + } catch { + print("Error resetting swiped profiles: \(error.localizedDescription)") + } + } + func fetchProfiles(currentUserID: String) async { let db = Firestore.firestore() await MainActor.run { self.isLoading = true } do { - // 1. Fetch the current user doc to grab its 'city' and swiped profiles + // Fetch the current user doc to grab its 'city' and swiped profiles let userDoc = try await db.collection("users").document(currentUserID).getDocument() let swipedProfiles = userDoc.data()?["swipedProfiles"] as? [String] ?? [] - // 2. Also figure out the city of this user (default to empty if missing) + // Also figure out the city of this user (default to empty if missing) let currentUserCity = userDoc.data()?["city"] as? String ?? "" - // 3. Also fetch existing matches so we can exclude them + // Also fetch existing matches so we can exclude them let matchesSnapshot = try await db.collection("matches").getDocuments() let matchedProfiles = matchesSnapshot.documents.compactMap { doc -> String? in let data = doc.data() @@ -69,16 +81,16 @@ class SwipeViewModel: ObservableObject { return nil } - // 4. Combine both sets of profiles to exclude + // Combine both sets of profiles to exclude let excludedProfiles = Set(swipedProfiles + matchedProfiles) - // 5. Fetch other profiles *only* from the same city + // Fetch other profiles *only* from the same city let snapshot = try await db.collection("users") .whereField("city", isEqualTo: currentUserCity) .whereField("id", isNotEqualTo: currentUserID) .getDocuments() - // 6. Decode and filter out excluded profiles + // Decode and filter out excluded profiles let profiles = snapshot.documents.compactMap { doc -> ProfileCardView.Model? in do { let profile = try doc.data(as: ProfileCardView.Model.self) From 44cc8639ae604baca6f9b61257bc783c6a23582a Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Tue, 4 Feb 2025 13:35:40 -0700 Subject: [PATCH 3/7] Fix auth issue causing a brief flash of the onboarding page before navigating to home. --- FlatMate/ContentView.swift | 64 ++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/FlatMate/ContentView.swift b/FlatMate/ContentView.swift index 60c04e1..e03de19 100644 --- a/FlatMate/ContentView.swift +++ b/FlatMate/ContentView.swift @@ -9,35 +9,65 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var viewModel: AuthViewModel + @State private var isLoadingUser = true + @State private var fakeProgress: Double = 0 // We'll animate this from 0 to 1 var body: some View { Group { - if viewModel.userSession != nil { - if viewModel.hasCompletedOnboarding { - MainView() - } else { - OnboardingPageView(onComplete: { - Task { - do { - try await viewModel.completeOnboarding() // Update Firebase and ViewModel - } catch { - print("DEBUG: Failed to mark onboarding as complete: \(error)") - } - } - }) + if isLoadingUser { + // Custom Loading Screen + VStack(spacing: 20) { + Text("Loading your data...") + .font(.headline) + + // Linear progress bar from 0..1 + ProgressView(value: fakeProgress, total: 1.0) + .progressViewStyle(LinearProgressViewStyle(tint: .blue)) + .frame(width: 200) + + // Optional extra text } + .onAppear { + // Animate our fake progress to 100% in ~2 seconds + withAnimation(.linear(duration: 2.0)) { + fakeProgress = 1.0 + } + } + } else { - NavigationStack { - LandingPageView() + // Once done loading, show real content + if viewModel.userSession != nil { + if viewModel.hasCompletedOnboarding { + MainView() + } else { + OnboardingPageView(onComplete: { + Task { + do { + try await viewModel.completeOnboarding() + } catch { + print("DEBUG: Failed to mark onboarding as complete: \(error)") + } + } + }) + } + } else { + NavigationStack { + LandingPageView() + } } } } - .onAppear { - print("ContentView appeared") + .task { + // fetch the user from Firebase + await viewModel.fetchUser() + // Once the data is in, turn the loading flag off + isLoadingUser = false } } } + + #Preview { ContentView() } From 8019ab04bfdec8031710628240338a2ec2f736b5 Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Tue, 4 Feb 2025 14:49:58 -0700 Subject: [PATCH 4/7] fixed the profile update thing --- FlatMate/View/EditProfileView.swift | 2 +- FlatMate/ViewModel/AuthViewModel.swift | 38 ++++++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/FlatMate/View/EditProfileView.swift b/FlatMate/View/EditProfileView.swift index 977ffd4..018c941 100644 --- a/FlatMate/View/EditProfileView.swift +++ b/FlatMate/View/EditProfileView.swift @@ -173,7 +173,7 @@ struct EditProfileView: View { updateSuccess = false } } message: { - Text("Profile Updated Successfully") + Text("Changes Updated Successfully") } } diff --git a/FlatMate/ViewModel/AuthViewModel.swift b/FlatMate/ViewModel/AuthViewModel.swift index 8896371..06e7803 100644 --- a/FlatMate/ViewModel/AuthViewModel.swift +++ b/FlatMate/ViewModel/AuthViewModel.swift @@ -149,7 +149,7 @@ class AuthViewModel: ObservableObject { func updateProfile( firstname: String, lastname: String, - dob: Date, // Updated to accept Date instead of age + dob: Date, age: Int, bio: String, isSmoker: Bool, @@ -160,13 +160,17 @@ class AuthViewModel: ObservableObject { noiseTolerance: Double, profileImage: UIImage? ) async throws { - guard let uid = userSession?.uid else { throw NSError(domain: "AuthError", code: 401, userInfo: [NSLocalizedDescriptionKey: "User not logged in"]) } + guard let uid = userSession?.uid else { + print("DEBUG: No logged in user. Cannot update profile.") + throw NSError(domain: "AuthError", code: 401, userInfo: [NSLocalizedDescriptionKey: "User not logged in"]) + } + + print("DEBUG: Updating profile for uid: \(uid)") - // Prepare updated data var updatedData: [String: Any] = [ "firstName": firstname, "lastName": lastname, - "dob": Timestamp(date: dob), // Store Date as Firestore Timestamp + "dob": Timestamp(date: dob), "age": age, "bio": bio, "isSmoker": isSmoker, @@ -178,22 +182,30 @@ class AuthViewModel: ObservableObject { ] do { - // Upload profile image if available + // Upload the profileImage if let profileImage = profileImage, let imageData = profileImage.jpegData(compressionQuality: 0.8) { + print("DEBUG: Uploading image to Storage for user \(uid)") let storageRef = Storage.storage().reference().child("profile_images/\(uid).jpg") - let _ = try await storageRef.putDataAsync(imageData) + + _ = try await storageRef.putDataAsync(imageData) let downloadURL = try await storageRef.downloadURL() + print("DEBUG: Got download URL:", downloadURL.absoluteString) updatedData["profileImageURL"] = downloadURL.absoluteString + } else { + print("DEBUG: No profileImage passed in, skipping image upload.") } - // Update Firestore document - try await Firestore.firestore().collection("users").document(uid).updateData(updatedData) + // Update Firestore user doc + print("DEBUG: Updating Firestore doc with:", updatedData) + let userRef = Firestore.firestore().collection("users").document(uid) + try await userRef.updateData(updatedData) + print("DEBUG: Successfully updated Firestore doc for \(uid)!") - // Update local `currentUser` with the new data + // Update local in-memory user if var currentUser = self.currentUser { currentUser.firstName = firstname currentUser.lastName = lastname - currentUser.dob = dob // Update dob directly + currentUser.dob = dob currentUser.bio = bio currentUser.isSmoker = isSmoker currentUser.pets = pets @@ -201,13 +213,15 @@ class AuthViewModel: ObservableObject { currentUser.partyFrequency = partyFrequency currentUser.guestFrequency = guestFrequency currentUser.noiseTolerance = noiseTolerance - if let profileImageURL = updatedData["profileImageURL"] as? String { - currentUser.profileImageURL = profileImageURL + if let url = updatedData["profileImageURL"] as? String { + currentUser.profileImageURL = url } self.currentUser = currentUser } } catch { + print("DEBUG: Error updating profile: \(error.localizedDescription)") throw error } } + } From 589c880a5baa737dd24a2ff58a02dde5bc71e218 Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Tue, 4 Feb 2025 16:09:27 -0700 Subject: [PATCH 5/7] Add Secrets.plist to .gitignore to prevent accidental commits and added the pixabay images --- .gitignore | 2 +- FlatMate/Models/PixabayImage.swift | 41 ++++++++ FlatMate/Secrets.template.plist | 8 ++ FlatMate/Services/PixabayService.swift | 51 ++++++++++ FlatMate/View/EditProfileView.swift | 47 +++++---- FlatMate/View/PixabaySearchView.swift | 126 +++++++++++++++++++++++++ 6 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 FlatMate/Models/PixabayImage.swift create mode 100644 FlatMate/Secrets.template.plist create mode 100644 FlatMate/Services/PixabayService.swift create mode 100644 FlatMate/View/PixabaySearchView.swift diff --git a/.gitignore b/.gitignore index e0889ee..bd45bb5 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,4 @@ fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output -.DS_Store \ No newline at end of file +.DS_StoreFlatMate/Secrets.plist diff --git a/FlatMate/Models/PixabayImage.swift b/FlatMate/Models/PixabayImage.swift new file mode 100644 index 0000000..87f68d2 --- /dev/null +++ b/FlatMate/Models/PixabayImage.swift @@ -0,0 +1,41 @@ +// +// PixabayImage.swift +// FlatMate +// +// Created by Ivan on 2025-02-04. +// + +struct PixabayResponse: Decodable { + let total: Int + let totalHits: Int + let hits: [PixabayImage] +} + +struct PixabayImage: Decodable, Identifiable { + let id: Int + let pageURL: String? + let type: String? + let tags: String? + + let previewURL: String? + let previewWidth: Int? + let previewHeight: Int? + + let webformatURL: String? + let webformatWidth: Int? + let webformatHeight: Int? + + let largeImageURL: String? + let imageWidth: Int? + let imageHeight: Int? + let imageSize: Int? + + let views: Int? + let downloads: Int? + let likes: Int? + let comments: Int? + + let user_id: Int? + let user: String? + let userImageURL: String? +} diff --git a/FlatMate/Secrets.template.plist b/FlatMate/Secrets.template.plist new file mode 100644 index 0000000..e3c4d6f --- /dev/null +++ b/FlatMate/Secrets.template.plist @@ -0,0 +1,8 @@ + + + + + PIXABAY_API_KEY + YOUR_PIXABAY_API_KEY + + diff --git a/FlatMate/Services/PixabayService.swift b/FlatMate/Services/PixabayService.swift new file mode 100644 index 0000000..d5ec6a6 --- /dev/null +++ b/FlatMate/Services/PixabayService.swift @@ -0,0 +1,51 @@ +// +// PixabayService.swift +// FlatMate +// +// Created by Ivan on 2025-02-04. +// + + +import Foundation +import Combine + +/// Represents the entire JSON response from Pixabay + + + +class PixabayService: ObservableObject { + + private lazy var apiKey: String = { + guard + let path = Bundle.main.path(forResource: "Secrets", ofType: "plist"), + let dict = NSDictionary(contentsOfFile: path) as? [String: Any], + let key = dict["PIXABAY_API_KEY"] as? String + else { + fatalError("Couldn't find 'PIXABAY_API_KEY' in Secrets.plist.") + } + return key + }() + + func fetchImages(query: String) -> AnyPublisher<[PixabayImage], Error> { + + let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query + + let urlString = "https://pixabay.com/api/?key=\(apiKey)&q=\(encodedQuery)&image_type=photo" + + guard let url = URL(string: urlString) else { + return Fail(error: URLError(.badURL)).eraseToAnyPublisher() + } + + return URLSession.shared.dataTaskPublisher(for: url) + .tryMap { (data, response) -> Data in + + return data + } + // Decode the JSON into `PixabayResponse` + .decode(type: PixabayResponse.self, decoder: JSONDecoder()) + .map { $0.hits } + .receive(on: DispatchQueue.main) + // Convert to an AnyPublisher + .eraseToAnyPublisher() + } +} diff --git a/FlatMate/View/EditProfileView.swift b/FlatMate/View/EditProfileView.swift index 018c941..8367339 100644 --- a/FlatMate/View/EditProfileView.swift +++ b/FlatMate/View/EditProfileView.swift @@ -28,10 +28,11 @@ struct EditProfileView: View { @State private var selectedGuestFrequency: String = frequencies[0] @State private var noiseTolerance: Double = 0.0 @State private var profileImage: UIImage? = nil - @State private var isImagePickerPresented = false +// @State private var isImagePickerPresented = false @State private var errorMessage: String? - @State private var selectedItem: PhotosPickerItem? = nil +// @State private var selectedItem: PhotosPickerItem? = nil @State private var updateSuccess: Bool = false + @State private var showPixabaySheet = false var body: some View { NavigationView { @@ -56,11 +57,11 @@ struct EditProfileView: View { .fill(Color.gray) .frame(width: 100, height: 100) } - PhotosPicker( - selection: $selectedItem, - matching: .images, - photoLibrary: .shared() - ) { + + Button { + // Show your pixabay sheet + showPixabaySheet = true + } label: { Image(systemName: "plus") .font(.system(size: 15, weight: .bold)) .foregroundColor(.white) @@ -68,18 +69,9 @@ struct EditProfileView: View { .background(Circle().fill(Color("primary"))) .shadow(radius: 5) } - .onChange(of: selectedItem) { oldValue, newValue in - Task { - if let data = try? await newValue?.loadTransferable(type: Data.self), - let uiImage = UIImage(data: data) { - DispatchQueue.main.async { - profileImage = uiImage - } - } - } - } .offset(x: 35, y: 35) } + } .padding(.trailing, 10) // First Name, Last Name, Date of Birth @@ -87,8 +79,8 @@ struct EditProfileView: View { ProfileField(title: "First Name", text: $firstName) ProfileField(title: "Last Name", text: $lastName) DatePicker("Date of Birth", selection: $dob, displayedComponents: .date) - .onChange(of: dob) { - age = calculateAge(from: dob) + .onChange(of: dob) { newDate in + age = calculateAge(from: newDate) } } } @@ -174,6 +166,23 @@ struct EditProfileView: View { } } message: { Text("Changes Updated Successfully") + }.sheet(isPresented: $showPixabaySheet) { + PixabaySearchView { selectedImage in + // The user tapped a PixabayImage. We can load its largeImageURL or webformatURL. + if let urlStr = selectedImage.largeImageURL ?? selectedImage.webformatURL, + let url = URL(string: urlStr) { + Task { + do { + let (data, _) = try await URLSession.shared.data(from: url) + if let downloadedImg = UIImage(data: data) { + self.profileImage = downloadedImg + } + } catch { + print("Error downloading chosen Pixabay image: \(error.localizedDescription)") + } + } + } + } } } diff --git a/FlatMate/View/PixabaySearchView.swift b/FlatMate/View/PixabaySearchView.swift new file mode 100644 index 0000000..d645e4b --- /dev/null +++ b/FlatMate/View/PixabaySearchView.swift @@ -0,0 +1,126 @@ +// +// PixabaySearchView.swift +// FlatMate +// +// Created by Ivan on 2025-02-04. +// + +import SwiftUI +import Combine + +class PixabaySearchViewModel: ObservableObject { + @Published var searchTerm: String = "" + @Published var images: [PixabayImage] = [] + @Published var isLoading: Bool = false + + private var cancellables = Set() + + private let service = PixabayService() + + init() { + + } + + // Called when user presses “Search” or modifies `searchTerm` + func search(_ term: String) { + guard !term.isEmpty else { + images = [] + return + } + isLoading = true + + service.fetchImages(query: term) + .sink { [weak self] completion in + guard let self = self else { return } + self.isLoading = false + + switch completion { + case .failure(let error): + print("Error fetching Pixabay: \(error.localizedDescription)") + self.images = [] + case .finished: + break + } + } receiveValue: { [weak self] hits in + self?.images = hits + } + .store(in: &cancellables) + } +} + +struct PixabaySearchView: View { + @Environment(\.dismiss) var dismiss + @StateObject private var vm = PixabaySearchViewModel() + + var onImageSelected: ((PixabayImage) -> Void)? + + var body: some View { + NavigationView { + VStack { + // Search bar + HStack { + TextField("Search images...", text: $vm.searchTerm) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onSubmit { + vm.search(vm.searchTerm) + } + Button("Search") { + vm.search(vm.searchTerm) + } + } + .padding() + + // Loading indicator + if vm.isLoading { + ProgressView("Loading...") + } + + // Grid of images + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 16)]) { + ForEach(vm.images) { pixabayImage in + // A simple AsyncImage for the preview + if let previewURL = pixabayImage.previewURL, + let url = URL(string: previewURL) { + AsyncImage(url: url) { phase in + switch phase { + case .empty: + ProgressView() + case .success(let image): + image + .resizable() + .scaledToFill() + case .failure: + Image(systemName: "photo") + .resizable() + .scaledToFill() + @unknown default: + EmptyView() + } + } + .frame(width: 100, height: 100) + .clipped() + .onTapGesture { + onImageSelected?(pixabayImage) + dismiss() + } + } else { + // If no previewURL, just a placeholder + Rectangle() + .fill(Color.gray) + .frame(width: 100, height: 100) + .onTapGesture { + onImageSelected?(pixabayImage) + dismiss() + } + } + } + } + .padding() + } + } + .navigationTitle("Pixabay Images") + .navigationBarTitleDisplayMode(.inline) + } + } +} From 02bacce67997e4a9dea3ba1c7d56c463f6fcb66b Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Tue, 4 Feb 2025 16:13:47 -0700 Subject: [PATCH 6/7] Ignore .DS_Store globally --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bd45bb5..641a743 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ fastlane/screenshots/**/*.png fastlane/test_output .DS_StoreFlatMate/Secrets.plist +.DS_Store From 44ee739cb7a24c23f1c011b59345b032d99986ed Mon Sep 17 00:00:00 2001 From: ShouravRakshit Date: Tue, 4 Feb 2025 16:17:00 -0700 Subject: [PATCH 7/7] Ensure Secrets.plist is ignored --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 641a743..00b2af5 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,5 @@ fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output -.DS_StoreFlatMate/Secrets.plist +FlatMate/Secrets.plist .DS_Store