diff --git a/.gitignore b/.gitignore
index e0889ee..00b2af5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,4 +61,5 @@ fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
-.DS_Store
\ No newline at end of file
+FlatMate/Secrets.plist
+.DS_Store
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()
}
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 977ffd4..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)
}
}
}
@@ -173,7 +165,24 @@ struct EditProfileView: View {
updateSuccess = false
}
} message: {
- Text("Profile Updated Successfully")
+ 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)
+ }
+ }
+}
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/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
}
}
+
}
diff --git a/FlatMate/ViewModel/SwipeViewModel.swift b/FlatMate/ViewModel/SwipeViewModel.swift
index a7c564b..c437a87 100644
--- a/FlatMate/ViewModel/SwipeViewModel.swift
+++ b/FlatMate/ViewModel/SwipeViewModel.swift
@@ -42,16 +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 {
- // Step 1: Fetch the user's 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] ?? []
- // Step 2: Also fetch existing matches to exclude them
+ // Also figure out the city of this user (default to empty if missing)
+ let currentUserCity = userDoc.data()?["city"] as? String ?? ""
+
+ // 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 +81,20 @@ class SwipeViewModel: ObservableObject {
return nil
}
- // Step 3: Combine both sets of profiles to exclude
+ // Combine both sets of profiles to exclude
let excludedProfiles = Set(swipedProfiles + matchedProfiles)
- // Step 4: Fetch and filter available profiles
+ // Fetch other profiles *only* from the same city
let snapshot = try await db.collection("users")
+ .whereField("city", isEqualTo: currentUserCity)
.whereField("id", isNotEqualTo: currentUserID)
.getDocuments()
+ // 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 +111,5 @@ class SwipeViewModel: ObservableObject {
await MainActor.run { self.isLoading = false }
}
}
+
}