Skip to content
Open
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
167 changes: 110 additions & 57 deletions FlatMate/Components/ProfileCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
// Created by Ben Schmidt on 2024-10-20.
//



import SwiftUI

struct ProfileCardView: View {

enum SwipeDirection {
case left, right, none
}

// The data model for each card
struct Model: Identifiable, Equatable, Codable {
let id: String
let firstName: String
Expand All @@ -20,12 +24,14 @@ struct ProfileCardView: View {
let bio: String
let roomState: String
let location: String
let profileImageURL: String? // Now optional
let profileImageURL: String?
let isSmoker: Bool
let petsOk: Bool
let noiseTolerance: Double
let partyFrequency: String
let guestFrequency: String

// We store swipeDirection (left, right, none) so we can track or animate it
var swipeDirection: SwipeDirection = .none

enum CodingKeys: String, CodingKey {
Expand All @@ -45,83 +51,82 @@ struct ProfileCardView: View {
}
}

private func renderTextWithEmoji(_ emoji: String, _ text: String) -> HStack<TupleView<(Text, Text)>> {
HStack(alignment: .top, spacing: 15) {
Text(emoji)
Text(text)
}
}

private func renderNoiseTolerance(_ noiseTolerance: Double) -> HStack<TupleView<(Text, Text)>> {
if noiseTolerance < 3 {
return renderTextWithEmoji(noiseTolerance > 0 ? "✅" : "🚫", "Noise")
}
return renderTextWithEmoji("🔊", "Unbothered by noise")
}

private func renderPartying(_ partyFrequency: String) -> HStack<TupleView<(Text, Text)>> {
switch partyFrequency {
case "Always":
return renderTextWithEmoji("🪩", "Party Animal")
case "Sometimes":
return renderTextWithEmoji("✅", "Partying")
default:
return renderTextWithEmoji("🚫", "No Parties")
}
}

private func renderGuests(_ guestFrequency: String) -> HStack<TupleView<(Text, Text)>> {
switch guestFrequency {
case "Always":
return renderTextWithEmoji("🧑‍🤝‍🧑", "Loves Guests")
case "Sometimes":
return renderTextWithEmoji("✅", "Occasional Guests")
default:
return renderTextWithEmoji("🚫", "No Guests")
}
}

var profile: Model
var size: CGSize
var dragOffset: CGSize
var isTopCard: Bool
var isSecondCard: Bool

let profile: Model
let size: CGSize
let dragOffset: CGSize
let isTopCard: Bool
let isSecondCard: Bool

var body: some View {
VStack(alignment: .leading) {

// The top image portion
ZStack(alignment: .bottomLeading) {
if let profileImageURL = profile.profileImageURL, let url = URL(string: profileImageURL) {
AsyncImage(url: url) { image in
image
.resizable()
.scaledToFill()
.frame(width: size.width, height: size.height * 0.65)
.clipShape(Rectangle())
} placeholder: {
ProgressView()

// Display a spinner placeholder while it loads, then fade it in.
if let profileImageURL = profile.profileImageURL,
let url = URL(string: profileImageURL) {

AsyncImage(url: url, transaction: Transaction(animation: .easeIn)) { phase in
switch phase {
case .empty:
// Show a loading spinner while fetching
ProgressView()
.frame(width: size.width, height: size.height * 0.65)
.background(Color.gray.opacity(0.3))

case .success(let image):
image
.resizable()
.scaledToFill()
.frame(width: size.width, height: size.height * 0.65)
.clipShape(Rectangle())
.transition(.opacity)

case .failure:
// If the image fails to load, show a placeholder
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFit()
.frame(width: size.width, height: size.height * 0.65)
.foregroundColor(.gray)
.background(Color.gray.opacity(0.2))

@unknown default:
EmptyView()
}
}

} else {
// Default placeholder for users without a profile image
// if there's no profileImageURL, show a default placeholder
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFit()
.frame(width: size.width, height: size.height * 0.65)
.foregroundColor(.gray)
.background(Color.gray.opacity(0.2))
.clipShape(Rectangle())
}

// The overlay: name, age, location, etc. inside a thin blur
VStack(alignment: .leading) {
Text("\(profile.firstName), \(profile.age)")
.font(.custom("Outfit-Bold", size: 32))
.colorInvert()
Text(profile.gender).colorInvert()
Text("\(profile.roomState) in \(profile.location)").colorInvert()
.fontWeight(.bold)
.lineLimit(1)
.foregroundColor(.black)

Text(profile.gender)
.foregroundColor(.black)

Text("\(profile.roomState) in \(profile.location)")
.foregroundColor(.black)
}
.padding()
.background(.ultraThinMaterial)
}

// bio and icons
VStack(alignment: .leading, spacing: 6) {
renderTextWithEmoji("🗣️", "“\(profile.bio)“")
.padding(.bottom)
Expand All @@ -133,11 +138,20 @@ struct ProfileCardView: View {
}
.padding()
}
// Card styling
.background(Color.white)
.cornerRadius(20)
.shadow(color: isTopCard ? getShadowColor() : (isSecondCard && dragOffset.width != 0 ? Color.gray.opacity(0.2) : Color.clear), radius: 10, x: 0, y: 3)
.shadow(
color: isTopCard
? getShadowColor()
: (isSecondCard && dragOffset.width != 0 ? Color.gray.opacity(0.2) : Color.clear),
radius: 10, x: 0, y: 3
)
}



// Return a colored shadow on drag: green if swiping right, red if swiping left
private func getShadowColor() -> Color {
if dragOffset.width > 0 {
return Color.green
Expand All @@ -147,4 +161,43 @@ struct ProfileCardView: View {
return Color.gray.opacity(0.2)
}
}

private func renderTextWithEmoji(_ emoji: String, _ text: String) -> HStack<TupleView<(Text, Text)>> {
HStack(alignment: .top, spacing: 15) {
Text(emoji)
Text(text)
}
}

// the noise value as text + emoji
private func renderNoiseTolerance(_ noiseTolerance: Double) -> HStack<TupleView<(Text, Text)>> {
if noiseTolerance < 3 {
return renderTextWithEmoji(noiseTolerance > 0 ? "✅" : "🚫", "Noise")
}
return renderTextWithEmoji("🔊", "Unbothered by noise")
}

// the partyFrequency as text + emoji
private func renderPartying(_ partyFrequency: String) -> HStack<TupleView<(Text, Text)>> {
switch partyFrequency {
case "Always":
return renderTextWithEmoji("🪩", "Party Animal")
case "Sometimes":
return renderTextWithEmoji("✅", "Partying")
default:
return renderTextWithEmoji("🚫", "No Parties")
}
}

// the guestFrequency as text + emoji
private func renderGuests(_ guestFrequency: String) -> HStack<TupleView<(Text, Text)>> {
switch guestFrequency {
case "Always":
return renderTextWithEmoji("🧑‍🤝‍🧑", "Loves Guests")
case "Sometimes":
return renderTextWithEmoji("✅", "Occasional Guests")
default:
return renderTextWithEmoji("🚫", "No Guests")
}
}
}
4 changes: 2 additions & 2 deletions FlatMate/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ struct User: Identifiable, Codable {
var dob: Date?
var bio: String?
var isSmoker: Bool?
var pets: Bool?
var petsOk: Bool?
var gender: String?
var partyFrequency: String?
var guestFrequency: String?
var noiseTolerance: Double?
var noise: Double?
var profileImageURL: String?
let hasCompletedOnboarding: Bool
}
Expand Down
10 changes: 5 additions & 5 deletions FlatMate/View/EditProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct EditProfileView: View {
@State private var selectedGender: String = genders[0]
@State private var selectedPartyFrequency: String = frequencies[0]
@State private var selectedGuestFrequency: String = frequencies[0]
@State private var noiseTolerance: Double = 0.0
@State private var noise: Double = 0.0
@State private var profileImage: UIImage? = nil
// @State private var isImagePickerPresented = false
@State private var errorMessage: String?
Expand Down Expand Up @@ -138,7 +138,7 @@ struct EditProfileView: View {
.font(.custom("Outfit-Bold", fixedSize: 15))
HStack {
Text("Quiet")
Slider(value: $noiseTolerance, in: 0...5, step: 0.1)
Slider(value: $noise, in: 0...5, step: 0.1)
.accentColor(Color("primary"))
Text("Loud")
}
Expand Down Expand Up @@ -219,7 +219,7 @@ struct EditProfileView: View {
selectedGender = data["gender"] as? String ?? genders[0]
selectedPartyFrequency = data["partyFrequency"] as? String ?? frequencies[0]
selectedGuestFrequency = data["guestFrequency"] as? String ?? frequencies[0]
noiseTolerance = data["noise"] as? Double ?? 0.0
noise = data["noise"] as? Double ?? 0.0

if let imageURLString = data["profileImageURL"] as? String,
let url = URL(string: imageURLString) {
Expand Down Expand Up @@ -250,11 +250,11 @@ struct EditProfileView: View {
age: age, // Send age to backend
bio: bio,
isSmoker: isSmoker,
pets: petsOk,
petsOk: petsOk,
gender: selectedGender,
partyFrequency: selectedPartyFrequency,
guestFrequency: selectedGuestFrequency,
noiseTolerance: noiseTolerance,
noise: noise,
profileImage: profileImage
)
errorMessage = nil
Expand Down
19 changes: 12 additions & 7 deletions FlatMate/ViewModel/AuthViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,15 @@ class AuthViewModel: ObservableObject {
}
}

@MainActor
func completeOnboarding() async throws {
guard let uid = userSession?.uid else { return }
do {
try await Firestore.firestore().collection("users").document(uid).updateData(["hasCompletedOnboarding": true])
try await Firestore.firestore()
.collection("users")
.document(uid)
.updateData(["hasCompletedOnboarding": true] as [String: Bool])

self.hasCompletedOnboarding = true
} catch {
print("DEBUG: Failed to update onboarding status with error \(error.localizedDescription)")
Expand All @@ -153,11 +158,11 @@ class AuthViewModel: ObservableObject {
age: Int,
bio: String,
isSmoker: Bool,
pets: Bool,
petsOk: Bool,
gender: String,
partyFrequency: String,
guestFrequency: String,
noiseTolerance: Double,
noise: Double,
profileImage: UIImage?
) async throws {
guard let uid = userSession?.uid else {
Expand All @@ -174,11 +179,11 @@ class AuthViewModel: ObservableObject {
"age": age,
"bio": bio,
"isSmoker": isSmoker,
"pets": pets,
"petsOk": petsOk,
"gender": gender,
"partyFrequency": partyFrequency,
"guestFrequency": guestFrequency,
"noiseTolerance": noiseTolerance
"noise": noise
]

do {
Expand Down Expand Up @@ -208,11 +213,11 @@ class AuthViewModel: ObservableObject {
currentUser.dob = dob
currentUser.bio = bio
currentUser.isSmoker = isSmoker
currentUser.pets = pets
currentUser.petsOk = petsOk
currentUser.gender = gender
currentUser.partyFrequency = partyFrequency
currentUser.guestFrequency = guestFrequency
currentUser.noiseTolerance = noiseTolerance
currentUser.noise = noise
if let url = updatedData["profileImageURL"] as? String {
currentUser.profileImageURL = url
}
Expand Down
Loading