From 696530651ae6007a809142679994a1207f8931ad Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:02:58 +0100 Subject: [PATCH 01/29] Added models and services for key functions of the app --- Development.xcconfig | 9 + MobileAcebook/Models/Comment.swift | 15 ++ MobileAcebook/Models/Post.swift | 17 ++ MobileAcebook/Models/User.swift | 7 +- MobileAcebook/Services/CommentService.swift | 82 +++++++++ MobileAcebook/Services/PostService.swift | 190 ++++++++++++++++++++ MobileAcebook/Services/UserService.swift | 109 +++++++++++ 7 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 Development.xcconfig create mode 100644 MobileAcebook/Models/Comment.swift create mode 100644 MobileAcebook/Models/Post.swift create mode 100644 MobileAcebook/Services/CommentService.swift create mode 100644 MobileAcebook/Services/PostService.swift create mode 100644 MobileAcebook/Services/UserService.swift diff --git a/Development.xcconfig b/Development.xcconfig new file mode 100644 index 00000000..5e42bbba --- /dev/null +++ b/Development.xcconfig @@ -0,0 +1,9 @@ +// +// Development.xcconfig +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 diff --git a/MobileAcebook/Models/Comment.swift b/MobileAcebook/Models/Comment.swift new file mode 100644 index 00000000..e3319180 --- /dev/null +++ b/MobileAcebook/Models/Comment.swift @@ -0,0 +1,15 @@ +// +// Comment.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +import Foundation + +struct Comment: Codable, Identifiable { + let id: String // Corresponds to the MongoDB ObjectId for the comment + let message: String // The text content of the comment + let createdAt: Date // The creation date of the comment + let createdBy: User // The user who created the comment +} diff --git a/MobileAcebook/Models/Post.swift b/MobileAcebook/Models/Post.swift new file mode 100644 index 00000000..90f95b9e --- /dev/null +++ b/MobileAcebook/Models/Post.swift @@ -0,0 +1,17 @@ +// +// Post.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +import Foundation + +struct Post: Codable, Identifiable { + let id: String + let message: String + let createdAt: String + let createdBy: User + let imgUrl: String? + let likes: [String] // List of user IDs who liked the post +} diff --git a/MobileAcebook/Models/User.swift b/MobileAcebook/Models/User.swift index ea748dd0..e75a5adf 100644 --- a/MobileAcebook/Models/User.swift +++ b/MobileAcebook/Models/User.swift @@ -4,8 +4,11 @@ // // Created by Josué Estévez Fernández on 01/10/2023. // +import Foundation -public struct User { +struct User: Codable, Identifiable { + let id: String + let email: String let username: String - let password: String + let imgUrl: String? } diff --git a/MobileAcebook/Services/CommentService.swift b/MobileAcebook/Services/CommentService.swift new file mode 100644 index 00000000..2d6274fb --- /dev/null +++ b/MobileAcebook/Services/CommentService.swift @@ -0,0 +1,82 @@ +// +// CommentService.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +import Foundation + +class CommentService { + static let shared = CommentService() + private let baseURL = "http://localhost:3000" + + private init() {} + + // Fetch comments for a specific post + func fetchComments(forPostId postId: String, token: String, completion: @escaping ([Comment]?, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/comments/\(postId)") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(nil, error) + return + } + + guard let data = data else { + completion(nil, NSError(domain: "DataError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data returned"])) + return + } + + do { + let comments = try JSONDecoder().decode([Comment].self, from: data) + completion(comments, nil) + } catch let jsonError { + completion(nil, jsonError) + } + } + + task.resume() + } + + // Create a new comment for a specific post + func createComment(message: String, forPostId postId: String, token: String, completion: @escaping (Bool, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/comments/\(postId)") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let body: [String: Any] = [ + "message": message, + "createdBy": token, + "underPost": postId, + "createdAt": Date().iso8601String() // Assuming you have a Date extension for ISO 8601 format + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch let encodingError { + completion(false, encodingError) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(false, error) + return + } + + completion(true, nil) + } + + task.resume() + } +} + diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift new file mode 100644 index 00000000..177c1547 --- /dev/null +++ b/MobileAcebook/Services/PostService.swift @@ -0,0 +1,190 @@ +// +// PostService.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// +import UIKit +import Foundation + +class PostService { + static let shared = PostService() + private let baseURL = "http://localhost:3000" + + private init() {} + + // Fetch all posts + func fetchPosts(completion: @escaping ([Post]?, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/posts") else { return } + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + completion(nil, error) + return + } + + guard let data = data else { + completion(nil, NSError(domain: "DataError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data returned"])) + return + } + + do { + let posts = try JSONDecoder().decode([Post].self, from: data) + completion(posts, nil) + } catch let jsonError { + completion(nil, jsonError) + } + } + + task.resume() + } + + // Create a new post with optional image + func createPost(message: String, image: UIImage?, token: String, completion: @escaping (Bool, Error?) -> Void) { + if let image = image { + // If the user selected an image, upload it to Cloudinary first + uploadImageToCloudinary(image: image) { url, error in + if let url = url { + // After getting the image URL, create the post with the image + self.createPostWithImage(message: message, imgUrl: url, token: token, completion: completion) + } else { + completion(false, error) + } + } + } else { + // If no image was selected, create the post without an image + self.createPostWithImage(message: message, imgUrl: nil, token: token, completion: completion) + } + } + + // Helper function to create post with or without image URL + private func createPostWithImage(message: String, imgUrl: String?, token: String, completion: @escaping (Bool, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/posts") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + // Assuming `token` contains the user ID or you have access to the user's ID + var body: [String: Any] = [ + "message": message, + "createdBy": token, // Assuming token is the user ID, replace if necessary + "imgUrl": imgUrl ?? NSNull() + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch let encodingError { + completion(false, encodingError) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(false, error) + return + } + + completion(true, nil) + } + + task.resume() + } + + // Upload image to Cloudinary + private func uploadImageToCloudinary(image: UIImage, completion: @escaping (String?, Error?) -> Void) { + guard let cloudName = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_CLOUD_NAME") as? String, + let uploadPreset = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_UPLOAD_PRESET") as? String else { + completion(nil, NSError(domain: "CloudinaryError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cloudinary credentials not found."])) + return + } + + let url = URL(string: "https://api.cloudinary.com/v1_1/\(cloudName)/image/upload")! + + var request = URLRequest(url: url) + request.httpMethod = "POST" + + let boundary = UUID().uuidString + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + var data = Data() + + // Add your unsigned Cloudinary preset + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"upload_preset\"\r\n\r\n".data(using: .utf8)!) + data.append("\(uploadPreset)\r\n".data(using: .utf8)!) + + // Add image data + if let imageData = image.jpegData(compressionQuality: 0.7) { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!) + data.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) + data.append(imageData) + data.append("\r\n".data(using: .utf8)!) + } + + data.append("--\(boundary)--\r\n".data(using: .utf8)!) + + request.httpBody = data + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(nil, error) + return + } + + guard let data = data else { + completion(nil, nil) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let url = json["secure_url"] as? String { + completion(url, nil) + } else { + completion(nil, nil) + } + } catch { + completion(nil, error) + } + } + + task.resume() + } + + // Update likes for a post + func updateLikes(postId: String, token: String, completion: @escaping (Bool, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/posts/\(postId)") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let body: [String: Any] = ["postId": postId] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch let encodingError { + completion(false, encodingError) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(false, error) + return + } + + completion(true, nil) + } + + task.resume() + } +} + + diff --git a/MobileAcebook/Services/UserService.swift b/MobileAcebook/Services/UserService.swift new file mode 100644 index 00000000..f461c050 --- /dev/null +++ b/MobileAcebook/Services/UserService.swift @@ -0,0 +1,109 @@ +// +// UserService.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +import Foundation + +class UserService { + static let shared = UserService() + private let baseURL = "http://localhost:3000" + + private init() {} + + func createUser(email: String, password: String, username: String, completion: @escaping (User?, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/users") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = ["email": email, "password": password, "username": username] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch let encodingError { + completion(nil, encodingError) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(nil, error) + return + } + + guard let data = data else { return } + + do { + let user = try JSONDecoder().decode(User.self, from: data) + completion(user, nil) + } catch let jsonError { + completion(nil, jsonError) + } + } + + task.resume() + } + + func getUserDetails(token: String, completion: @escaping (User?, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/users") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(nil, error) + return + } + + guard let data = data else { return } + + do { + let user = try JSONDecoder().decode(User.self, from: data) + completion(user, nil) + } catch let jsonError { + completion(nil, jsonError) + } + } + + task.resume() + } + + func updateProfilePicture(token: String, imgUrl: String, completion: @escaping (Bool, Error?) -> Void) { + guard let url = URL(string: "\(baseURL)/users") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let body: [String: Any] = ["imgUrl": imgUrl] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch let encodingError { + completion(false, encodingError) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(false, error) + return + } + + completion(true, nil) + } + + task.resume() + } +} + From d0b6475e2f7a67c334139ba2085f41b7747190a9 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 3 Sep 2024 12:16:59 +0100 Subject: [PATCH 02/29] Sign Up View Added --- .DS_Store | Bin 6148 -> 6148 bytes MobileAcebook.xcodeproj/project.pbxproj | 8 ++ MobileAcebook/LoginView.swift | 8 ++ MobileAcebook/SignUpView.swift | 109 ++++++++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 MobileAcebook/LoginView.swift create mode 100644 MobileAcebook/SignUpView.swift diff --git a/.DS_Store b/.DS_Store index 7fa1be84d357e72af5ed943ecd6fe9ee00e303cc..aa44966853af418bd5abc15663ce08842a8d08f5 100644 GIT binary patch delta 53 zcmZoMXfc@JFUrcmz`)4BAi%(o!;s40$dC*qlQuK5EN5h7-Q33H$hi4GGY8AWhE1E< IIsWnk03q!R83}%aG!klb@WFlb-}s4K#(p4v6*sg8`7mz`(=c&EN^t;SDs$4G3}RTMtz0f~1dk qvjCGVqb3hS5Q8g&BZCWrFOb$_NM%UDYVG>XcFf`|o7p-3@&f?fUoQLr diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 5506db3b..6a544280 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 25F17D2D2C870C9F001CEF06 /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F17D2C2C870C9F001CEF06 /* SignUpView.swift */; }; + 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F17D2E2C870CAD001CEF06 /* LoginView.swift */; }; AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85AF2AC8A221009680C6 /* MobileAcebookApp.swift */; }; AE5D85B42AC8A224009680C6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE5D85B32AC8A224009680C6 /* Assets.xcassets */; }; AE5D85B72AC8A224009680C6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE5D85B62AC8A224009680C6 /* Preview Assets.xcassets */; }; @@ -39,6 +41,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 25F17D2C2C870C9F001CEF06 /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; + 25F17D2E2C870CAD001CEF06 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; AE5D85AC2AC8A221009680C6 /* MobileAcebook.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MobileAcebook.app; sourceTree = BUILT_PRODUCTS_DIR; }; AE5D85AF2AC8A221009680C6 /* MobileAcebookApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileAcebookApp.swift; sourceTree = ""; }; AE5D85B32AC8A224009680C6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -111,6 +115,8 @@ AE5D85B32AC8A224009680C6 /* Assets.xcassets */, AE5D85B52AC8A224009680C6 /* Preview Content */, AE5D85D92AC8A337009680C6 /* WelcomePageView.swift */, + 25F17D2C2C870C9F001CEF06 /* SignUpView.swift */, + 25F17D2E2C870CAD001CEF06 /* LoginView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -305,9 +311,11 @@ buildActionMask = 2147483647; files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, + 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */, AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, AE5D85E82AC9B29A009680C6 /* User.swift in Sources */, + 25F17D2D2C870C9F001CEF06 /* SignUpView.swift in Sources */, AE5D85DA2AC8A337009680C6 /* WelcomePageView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift new file mode 100644 index 00000000..477a8a5d --- /dev/null +++ b/MobileAcebook/LoginView.swift @@ -0,0 +1,8 @@ +// +// LoginView.swift +// MobileAcebook +// +// Created by William Alexander on 03/09/2024. +// + +import Foundation diff --git a/MobileAcebook/SignUpView.swift b/MobileAcebook/SignUpView.swift new file mode 100644 index 00000000..635077ac --- /dev/null +++ b/MobileAcebook/SignUpView.swift @@ -0,0 +1,109 @@ +// +// SignUpView.swift +// MobileAcebook +// +// Created by William Alexander on 03/09/2024. +// + +import Foundation +import SwiftUI +struct Constants { + static let ColorsBlue: Color = Color(red: 0, green: 0.48, blue: 1) + static let GraysWhite: Color = .white +} + +struct SignUpView: View { + func submit() -> Void { + print("Submitted") + + } + @State private var username: String = "" + @State private var email: String = "" + @State private var password: String = "" + var body: some View { + + VStack { + Text("Sign Up!") + .font( + .system(size: 40, weight: .bold, design: .default)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + + .frame(width: 288, height: 79, alignment: .center) + VStack { + VStack { + + TextField( + "Enter Username", + text: $username + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + .font(Font.custom("SF Pro", size: 17)) + Spacer() + TextField( + "Enter Email", + text: $email + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + Spacer() + TextField( + "Enter Password", + text: $password + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + } + .padding(0) + .padding(.bottom) + .frame(width: 302, height: 242, alignment: .center) + .cornerRadius(10) + HStack(alignment: .center, spacing: 3) { Button(action: submit) { + Text("Sign Up!") + .font(Font.custom("SF Pro", size: 20)) + .foregroundColor(Constants.GraysWhite) + } } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: 113, height: 48, alignment: .center) + .background(Constants.ColorsBlue) + .cornerRadius(40) + + HStack(alignment: .center, spacing: 0) { Text("Already have an account?
Login") + .font(Font.custom("SF Pro", size: 18)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) + .frame(width: 272, height: 43, alignment: .top) } + .padding(0) + .frame(width: 272, height: 43, alignment: .center) + } + .frame(width: 335, height: 432) + .background(.white.opacity(0.75)) + + .cornerRadius(48) + } + + + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0, green: 0.96, blue: 1)) + .statusBar(hidden: false) + } + +} + + +struct SignUpView_Previews: PreviewProvider { + static var previews: some View { + SignUpView() + } +} From 379c95eec5e3717c1feeb2ab84db7193e6ae58e6 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:10:56 +0100 Subject: [PATCH 03/29] Added welcome view styling a code --- MobileAcebook/WelcomePageView.swift | 69 ++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/WelcomePageView.swift index 96006af9..e60ba913 100644 --- a/MobileAcebook/WelcomePageView.swift +++ b/MobileAcebook/WelcomePageView.swift @@ -9,34 +9,53 @@ import SwiftUI struct WelcomePageView: View { var body: some View { - ZStack { - VStack { - Spacer() - - Text("Welcome to Acebook!") - .font(.largeTitle) - .padding(.bottom, 20) - .accessibilityIdentifier("welcomeText") - - Spacer() - - Image("makers-logo") - .resizable() - .scaledToFit() - .frame(width: 200, height: 200) - .accessibilityIdentifier("makers-logo") - - Spacer() - - Button("Sign Up") { - // TODO: sign up logic + NavigationView { + VStack { + Spacer() + + Text("Acebook") + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.black) + + Spacer() + + Text("You are not logged in.\nPlease login or sign up") + .multilineTextAlignment(.center) + .padding() + .background(Color.white.opacity(0.8)) + .cornerRadius(10) + .padding(.horizontal) + + HStack { + NavigationLink(destination: SignUpView()) { + Text("Sign Up") + .foregroundColor(.blue) + .padding() + .frame(maxWidth: .infinity) + .background(Color.white.opacity(0.8)) + .cornerRadius(10) + .padding(.horizontal, 5) + } + + NavigationLink(destination: LoginView()) { + Text("Login") + .foregroundColor(.blue) + .padding() + .frame(maxWidth: .infinity) + .background(Color.white.opacity(0.8)) + .cornerRadius(10) + .padding(.horizontal, 5) + } + } + .padding() + + Spacer() } - .accessibilityIdentifier("signUpButton") - - Spacer() + .background(Color.cyan) + .edgesIgnoringSafeArea(.all) } } - } } struct WelcomePageView_Previews: PreviewProvider { From e0423bda667f2e2e6a4b45e141f2f5f2df151d18 Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 3 Sep 2024 13:19:17 +0100 Subject: [PATCH 04/29] add create new post view --- MobileAcebook/CreatePostView.swift | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 MobileAcebook/CreatePostView.swift diff --git a/MobileAcebook/CreatePostView.swift b/MobileAcebook/CreatePostView.swift new file mode 100644 index 00000000..adb92d4b --- /dev/null +++ b/MobileAcebook/CreatePostView.swift @@ -0,0 +1,49 @@ +// +// CreatePostView.swift +// MobileAcebook +// +// Created by Maz on 03/09/2024. +// + +import SwiftUI + +struct CreatePostView: View { + @State private var userInput: String = "" + var body: some View { + VStack(alignment: .center){ + Text("Make a Post").font(.largeTitle).bold() + TextField( + "Post text, lorem ipsum day...", + text: $userInput, + axis: .vertical + ).textFieldStyle(.roundedBorder) + .lineLimit(10, reservesSpace: true) + .multilineTextAlignment(.leading) + .frame(minWidth: 100, maxWidth: 400, minHeight: 100, maxHeight: 250) +// .cornerRadius(40) + HStack(alignment: .center, spacing: 3){ + Button("Add Image"){} + .frame(width: 96, height: 64) + .background(Color(red: 0, green: 0.48, blue: 1)) + .cornerRadius(40) + .foregroundColor(.white) + + Spacer() + Button("Create Post"){} + .frame(width: 96, height: 64) + .background(Color(red: 0, green: 0.48, blue: 1)) + .cornerRadius(40) + .foregroundColor(.white) + + }.padding(40) + + }.frame(maxHeight: 900) + .padding() + .background(Color(red: 0, green: 0.96, blue: 1)) + + } +} + +#Preview { + CreatePostView() +} From a9425a01b5e5af787ed532e5eeb822c1ced2f826 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:32:41 +0100 Subject: [PATCH 05/29] Update User.swift Removed id for user creation --- MobileAcebook/Models/User.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MobileAcebook/Models/User.swift b/MobileAcebook/Models/User.swift index e75a5adf..f2d47e42 100644 --- a/MobileAcebook/Models/User.swift +++ b/MobileAcebook/Models/User.swift @@ -7,7 +7,6 @@ import Foundation struct User: Codable, Identifiable { - let id: String let email: String let username: String let imgUrl: String? From 0d9371eac076020c3dc1bf51aed89010738b7b9f Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:33:02 +0100 Subject: [PATCH 06/29] Update Comment.swift Id removal --- MobileAcebook/Models/Comment.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MobileAcebook/Models/Comment.swift b/MobileAcebook/Models/Comment.swift index e3319180..9d5a7deb 100644 --- a/MobileAcebook/Models/Comment.swift +++ b/MobileAcebook/Models/Comment.swift @@ -8,7 +8,6 @@ import Foundation struct Comment: Codable, Identifiable { - let id: String // Corresponds to the MongoDB ObjectId for the comment let message: String // The text content of the comment let createdAt: Date // The creation date of the comment let createdBy: User // The user who created the comment From 438c17ea59f35be913cee8386ae9d4b94fda91c5 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:33:23 +0100 Subject: [PATCH 07/29] Update Post.swift removed id --- MobileAcebook/Models/Post.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MobileAcebook/Models/Post.swift b/MobileAcebook/Models/Post.swift index 90f95b9e..d40dc7b8 100644 --- a/MobileAcebook/Models/Post.swift +++ b/MobileAcebook/Models/Post.swift @@ -8,7 +8,6 @@ import Foundation struct Post: Codable, Identifiable { - let id: String let message: String let createdAt: String let createdBy: User From eb839c36217bf82a8f2925d3c179e38c184bc4d1 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:30:04 +0100 Subject: [PATCH 08/29] Added ds fiel changes --- .DS_Store | Bin 6148 -> 6148 bytes MobileAcebook/.DS_Store | Bin 0 -> 8196 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 MobileAcebook/.DS_Store diff --git a/.DS_Store b/.DS_Store index aa44966853af418bd5abc15663ce08842a8d08f5..512607ca9014a489cf905578b37f2b84d1c88c48 100644 GIT binary patch delta 621 zcmZoMXfc=|#>B!ku~2NHo}wr(0|Nsi1A_nqLk>eeLjglBLn=ew=0N7<%$^`A5e65a zKp7AtNi&o%=rL3F|Mt z^pJ$)>XXWg3!r*-B^Bgk7MB$S)5rNh~QXc1kRY2Ju4j z^K+75?8Kz7%+&ID0TJi?ypqJsywoDFhRl>yppuyI%)FHRa;N;#yp&?F-e8CX2PX$- zyns}-d$pm3g@KNOp|PoDt&T#qr4f*0VrEiX%gG_CtZy9@pPiGNm)`^Q3J@>?LkSE( z!Ga8Wf!>o7C<`vi%gN762g-mHicc0`lA4^tq{POs9>~?3Y{z6Jl9O&2oSdIq0M^a8 zlLtwih(vC_iwiU`INHCLCN6S51`0%&K_CSwcom2gWFQ&O1hzy7?2JuJlB}{ozTV^u zOipZ!-C(8!vx*(#PY@FpCqOnQLlA>2Fs57>e1WtcFg2v0Ml2J^gHRn43)MHXbMSKj Z6B97Od}p4_FXG4n^gP(=%@HDNm;q-MoreGb delta 180 zcmZoMXfc=|#>B)qu~2NHo}wr#0|Nsi1A_nqLk>eKgCj#SkWAWGxSWxZb#fnzoQ!8q zesWSyeiBd_Pz|FjkaqbG1`G_7=dd~mF?J&h0og*67qCiCmSmS;{RxtpY{u@y#*_|b z*0HPb0PSF4*a9(Pvml2U%fyD2o7p+|Ie=~g^1m}r<`;3~0IC5wi)C|!$Qot<;=M5# diff --git a/MobileAcebook/.DS_Store b/MobileAcebook/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0fe756685ee1ad6237ab614ed6654d387700540f GIT binary patch literal 8196 zcmeHM&2G~`5S~p#>!^a{&{h=^k|nNHNuZ!sT#~dLkU*+1v2j!Q(WJ?J)wwu#6>6}q+#@tY#9 zi&`ipJ#q`00Z)`tLc4TDXXF;F&46LRFkl!k3>XFs1Ixew-r1s9bKd*vs*#2P!@z&Z zfOtOGs6scjo+vFJ9S|}FfG%TL7POHENQ|v@Q|pP+5(=Lxdte&Mbc(?g9M^4O4&Bsx zqO^h&Q*dH>W~MU~CT9oEmgdBoN*ieyFbotK5V?DmTFBcI@@Mk*>B!TNpLrqoU?~n) z3w8IWENBsJ&>_XZx46q0R1Ji ziY?&9nze4dbw2Q+le+03X}j@j(R;~x=v}v6?ukYB62M-U%JND?{K014k_QvCiwR!j6 z;}@Ot@FL=$umcpPAEb0s^{4VPS~I)4bmA!FQBQzStBA3P_oa@ojreoKOhg|t$JY-a zvfNfAn!}nSM!d*yNz9W)SmeNSEawwZ%|%4skJ&q+VE@lVrLePHWNKh4zzK@z74G+i zCs5wYS>`tB6#RxVE*EGW<4pIU7kVObmZzS~`yXtWVi;I<23FJpw?+Q{Wd8U6W#5Kb zpJBi-@COXA@{x1YhJ))r3G=>0u5F{fLls5XO_Y`p2$_yU%XA!i`VT|2ZI}v7Q|pP+ S7(p=~0wfJa7zX|-1HS-mb1TFE literal 0 HcmV?d00001 From 8470d4ce0c3b1304d8ff55f69de8ec0b56ac659c Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:36:27 +0100 Subject: [PATCH 09/29] Added back files to project pbxproj file --- MobileAcebook.xcodeproj/project.pbxproj | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 6a544280..e22cd077 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -21,6 +21,11 @@ AE5D85E32AC9AFD2009680C6 /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */; }; AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */; }; AE5D85E82AC9B29A009680C6 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E72AC9B29A009680C6 /* User.swift */; }; + F844A8AC2C874802007EA48A /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8A92C874802007EA48A /* PostService.swift */; }; + F844A8AD2C874802007EA48A /* CommentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AA2C874802007EA48A /* CommentService.swift */; }; + F844A8AE2C874802007EA48A /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AB2C874802007EA48A /* UserService.swift */; }; + F844A8B12C87480F007EA48A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AF2C87480F007EA48A /* Post.swift */; }; + F844A8B22C87480F007EA48A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B02C87480F007EA48A /* Comment.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -58,6 +63,11 @@ AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; AE5D85E72AC9B29A009680C6 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + F844A8A92C874802007EA48A /* PostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = ""; }; + F844A8AA2C874802007EA48A /* CommentService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentService.swift; sourceTree = ""; }; + F844A8AB2C874802007EA48A /* UserService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; + F844A8AF2C87480F007EA48A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + F844A8B02C87480F007EA48A /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -151,6 +161,9 @@ AE5D85DD2AC9AF72009680C6 /* Services */ = { isa = PBXGroup; children = ( + F844A8AA2C874802007EA48A /* CommentService.swift */, + F844A8A92C874802007EA48A /* PostService.swift */, + F844A8AB2C874802007EA48A /* UserService.swift */, AE5D85E02AC9AFA9009680C6 /* AuthenticationService.swift */, ); path = Services; @@ -167,6 +180,8 @@ AE5D85DF2AC9AF83009680C6 /* Models */ = { isa = PBXGroup; children = ( + F844A8B02C87480F007EA48A /* Comment.swift */, + F844A8AF2C87480F007EA48A /* Post.swift */, AE5D85E72AC9B29A009680C6 /* User.swift */, ); path = Models; @@ -311,9 +326,14 @@ buildActionMask = 2147483647; files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, + F844A8B12C87480F007EA48A /* Post.swift in Sources */, 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */, AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */, + F844A8AC2C874802007EA48A /* PostService.swift in Sources */, + F844A8AD2C874802007EA48A /* CommentService.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, + F844A8B22C87480F007EA48A /* Comment.swift in Sources */, + F844A8AE2C874802007EA48A /* UserService.swift in Sources */, AE5D85E82AC9B29A009680C6 /* User.swift in Sources */, 25F17D2D2C870C9F001CEF06 /* SignUpView.swift in Sources */, AE5D85DA2AC8A337009680C6 /* WelcomePageView.swift in Sources */, From b8d4fc245c5a42d3f7967073a004ae70c68e0b8f Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:58:43 +0100 Subject: [PATCH 10/29] Added back CreatePostView to xproj file --- .DS_Store | Bin 6148 -> 6148 bytes MobileAcebook.xcodeproj/project.pbxproj | 4 ++++ MobileAcebook/.DS_Store | Bin 8196 -> 8196 bytes MobileAcebook/Models/.DS_Store | Bin 0 -> 6148 bytes 4 files changed, 4 insertions(+) create mode 100644 MobileAcebook/Models/.DS_Store diff --git a/.DS_Store b/.DS_Store index 512607ca9014a489cf905578b37f2b84d1c88c48..6588252437145a9af8ef5a488082265e7479a1f0 100644 GIT binary patch delta 25 hcmZoMXffEJ#m2Pp{bU_BJ64Y!)0ZvYoX<8@5CD13384T0 delta 23 fcmZoMXffEJ#m2OgXR;2P-Q*2yR-2cwB?tllS_lVZ diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index e22cd077..bb78a811 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ F844A8AE2C874802007EA48A /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AB2C874802007EA48A /* UserService.swift */; }; F844A8B12C87480F007EA48A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AF2C87480F007EA48A /* Post.swift */; }; F844A8B22C87480F007EA48A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B02C87480F007EA48A /* Comment.swift */; }; + F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B52C874D56007EA48A /* CreatePostView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,6 +69,7 @@ F844A8AB2C874802007EA48A /* UserService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; F844A8AF2C87480F007EA48A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; F844A8B02C87480F007EA48A /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + F844A8B52C874D56007EA48A /* CreatePostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -127,6 +129,7 @@ AE5D85D92AC8A337009680C6 /* WelcomePageView.swift */, 25F17D2C2C870C9F001CEF06 /* SignUpView.swift */, 25F17D2E2C870CAD001CEF06 /* LoginView.swift */, + F844A8B52C874D56007EA48A /* CreatePostView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -327,6 +330,7 @@ files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, F844A8B12C87480F007EA48A /* Post.swift in Sources */, + F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */, 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */, AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */, F844A8AC2C874802007EA48A /* PostService.swift in Sources */, diff --git a/MobileAcebook/.DS_Store b/MobileAcebook/.DS_Store index 0fe756685ee1ad6237ab614ed6654d387700540f..88a9f6782acc5f5c6f33c3b0abeb2a827bca7b7f 100644 GIT binary patch delta 43 pcmZp1XmQwZLy*hJ$kaeb!O+;;cyfiX{bofWUq&Rc%@x8uJOB(c3u6EP delta 43 pcmZp1XmQwZLy*hR!oomD!O+;$a&m>R{bofWUq&Rc%@x8uJOB$e=tEjcq8!#3_cPQWMja1-5a0Dr%3ff!n%0%pxvaq zW_r4I*g9;_0FcT1;}tLhFr+JDqoHrQuHLhg$S8_E;|WWwF~$lXz3Sr$ z)S>1FkdutR_R}SqmMt$I0DWvBXhOi^B8CQjbp?NsX!`_3Zw$5z`s?1JzH&f z?wBzZNCi@Xy#o4uD0Iad*gD#$gF%n)x;{`;vPG`PE zT@7p Date: Tue, 3 Sep 2024 15:29:18 +0100 Subject: [PATCH 11/29] Identifiable switch --- MobileAcebook/Models/Comment.swift | 1 + MobileAcebook/Models/Post.swift | 1 + MobileAcebook/Models/User.swift | 1 + 3 files changed, 3 insertions(+) diff --git a/MobileAcebook/Models/Comment.swift b/MobileAcebook/Models/Comment.swift index 9d5a7deb..c914a91c 100644 --- a/MobileAcebook/Models/Comment.swift +++ b/MobileAcebook/Models/Comment.swift @@ -8,6 +8,7 @@ import Foundation struct Comment: Codable, Identifiable { + let id: String let message: String // The text content of the comment let createdAt: Date // The creation date of the comment let createdBy: User // The user who created the comment diff --git a/MobileAcebook/Models/Post.swift b/MobileAcebook/Models/Post.swift index d40dc7b8..90f95b9e 100644 --- a/MobileAcebook/Models/Post.swift +++ b/MobileAcebook/Models/Post.swift @@ -8,6 +8,7 @@ import Foundation struct Post: Codable, Identifiable { + let id: String let message: String let createdAt: String let createdBy: User diff --git a/MobileAcebook/Models/User.swift b/MobileAcebook/Models/User.swift index f2d47e42..e75a5adf 100644 --- a/MobileAcebook/Models/User.swift +++ b/MobileAcebook/Models/User.swift @@ -7,6 +7,7 @@ import Foundation struct User: Codable, Identifiable { + let id: String let email: String let username: String let imgUrl: String? From 86a4bee6897e6d450767c275ff90cea51ca0ddc6 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:32:50 +0100 Subject: [PATCH 12/29] Added date extension --- MobileAcebook.xcodeproj/project.pbxproj | 4 ++++ MobileAcebook/DateExtension.swift | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 MobileAcebook/DateExtension.swift diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index bb78a811..3df6d394 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ F844A8B12C87480F007EA48A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AF2C87480F007EA48A /* Post.swift */; }; F844A8B22C87480F007EA48A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B02C87480F007EA48A /* Comment.swift */; }; F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B52C874D56007EA48A /* CreatePostView.swift */; }; + F844A8B82C875538007EA48A /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B72C875538007EA48A /* DateExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -70,6 +71,7 @@ F844A8AF2C87480F007EA48A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; F844A8B02C87480F007EA48A /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; F844A8B52C874D56007EA48A /* CreatePostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostView.swift; sourceTree = ""; }; + F844A8B72C875538007EA48A /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -130,6 +132,7 @@ 25F17D2C2C870C9F001CEF06 /* SignUpView.swift */, 25F17D2E2C870CAD001CEF06 /* LoginView.swift */, F844A8B52C874D56007EA48A /* CreatePostView.swift */, + F844A8B72C875538007EA48A /* DateExtension.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -329,6 +332,7 @@ buildActionMask = 2147483647; files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, + F844A8B82C875538007EA48A /* DateExtension.swift in Sources */, F844A8B12C87480F007EA48A /* Post.swift in Sources */, F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */, 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */, diff --git a/MobileAcebook/DateExtension.swift b/MobileAcebook/DateExtension.swift new file mode 100644 index 00000000..d06ac05e --- /dev/null +++ b/MobileAcebook/DateExtension.swift @@ -0,0 +1,16 @@ +// +// DateExtension.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + +import Foundation + +extension Date { + func iso8601String() -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: self) + } +} From 8a7bbe3f28a965b7c8e569cd9742a786ae933a97 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:34:34 +0100 Subject: [PATCH 13/29] Commented out auth service --- .../Services/AuthenticationService.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/MobileAcebook/Services/AuthenticationService.swift b/MobileAcebook/Services/AuthenticationService.swift index 9f7181c3..61a7e6e9 100644 --- a/MobileAcebook/Services/AuthenticationService.swift +++ b/MobileAcebook/Services/AuthenticationService.swift @@ -1,13 +1,13 @@ +//// +//// AuthenticationService.swift +//// MobileAcebook +//// +//// Created by Josué Estévez Fernández on 01/10/2023. +//// // -// AuthenticationService.swift -// MobileAcebook -// -// Created by Josué Estévez Fernández on 01/10/2023. -// - -class AuthenticationService: AuthenticationServiceProtocol { - func signUp(user: User) -> Bool { - // Logic to call the backend API for signing up - return true // placeholder - } -} +//class AuthenticationService: AuthenticationServiceProtocol { +// func signUp(user: User) -> Bool { +// // Logic to call the backend API for signing up +// return true // placeholder +// } +//} From 4cee03821be2779e52296732f3880de2bd273931 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:38:35 +0100 Subject: [PATCH 14/29] Commented out stuff --- .../AuthenticationServiceProtocol.swift | 10 +++++----- MobileAcebook/Services/PostService.swift | 2 +- .../Services/MockAuthenticationService.swift | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift b/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift index ae012f49..dad84533 100644 --- a/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift +++ b/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift @@ -2,9 +2,9 @@ // AuthenticationServiceProtocol.swift // MobileAcebook // -// Created by Josué Estévez Fernández on 01/10/2023. +//// Created by Josué Estévez Fernández on 01/10/2023. +//// // - -public protocol AuthenticationServiceProtocol { - func signUp(user: User) -> Bool -} +//public protocol AuthenticationServiceProtocol { +// func signUp(user: User) -> Bool +//} diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index 177c1547..e6e08463 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -67,7 +67,7 @@ class PostService { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") // Assuming `token` contains the user ID or you have access to the user's ID - var body: [String: Any] = [ + let body: [String: Any] = [ "message": message, "createdBy": token, // Assuming token is the user ID, replace if necessary "imgUrl": imgUrl ?? NSNull() diff --git a/MobileAcebookTests/Services/MockAuthenticationService.swift b/MobileAcebookTests/Services/MockAuthenticationService.swift index 29a608e0..8d75d02d 100644 --- a/MobileAcebookTests/Services/MockAuthenticationService.swift +++ b/MobileAcebookTests/Services/MockAuthenticationService.swift @@ -1,15 +1,15 @@ // // MockAuthenticationService.swift // MobileAcebookTests +//// +//// Created by Josué Estévez Fernández on 01/10/2023. +//// // -// Created by Josué Estévez Fernández on 01/10/2023. +//@testable import MobileAcebook // - -@testable import MobileAcebook - -class MockAuthenticationService: AuthenticationServiceProtocol { - func signUp(user: User) -> Bool { - // Mocked logic for unit tests - return true // placeholder - } -} +//class MockAuthenticationService: AuthenticationServiceProtocol { +// func signUp(user: User) -> Bool { +// // Mocked logic for unit tests +// return true // placeholder +// } +//} From 9fba400ff57329cbde47eb4ac05ffad75b645513 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 3 Sep 2024 15:42:19 +0100 Subject: [PATCH 15/29] Added login view --- MobileAcebook/LoginView.swift | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index 477a8a5d..b1be4b99 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -6,3 +6,100 @@ // import Foundation +import SwiftUI + +struct LoginView: View { + func submit() -> Void { + print("Submitted") + + } + @State private var username: String = "" + @State private var email: String = "" + @State private var password: String = "" + var body: some View { + + VStack { + Text("Sign Up!") + .font( + .system(size: 40, weight: .bold, design: .default)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + + .frame(width: 288, height: 79, alignment: .center) + VStack { + VStack { + + TextField( + "Enter Username", + text: $username + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + .font(Font.custom("SF Pro", size: 17)) + Spacer() + TextField( + "Enter Email", + text: $email + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + Spacer() + TextField( + "Enter Password", + text: $password + ) + .padding(.leading, 16) + .padding(.trailing, 0) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + } + .padding(0) + .padding(.bottom) + .frame(width: 302, height: 242, alignment: .center) + .cornerRadius(10) + HStack(alignment: .center, spacing: 3) { Button(action: submit) { + Text("Sign Up!") + .font(Font.custom("SF Pro", size: 20)) + .foregroundColor(Constants.GraysWhite) + } } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: 113, height: 48, alignment: .center) + .background(Constants.ColorsBlue) + .cornerRadius(40) + + HStack(alignment: .center, spacing: 0) { Text("Already have an account?
Login") + .font(Font.custom("SF Pro", size: 18)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) + .frame(width: 272, height: 43, alignment: .top) } + .padding(0) + .frame(width: 272, height: 43, alignment: .center) + } + .frame(width: 335, height: 432) + .background(.white.opacity(0.75)) + + .cornerRadius(48) + } + + + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0, green: 0.96, blue: 1)) + .statusBar(hidden: false) + } + +} + + +struct LoginView_Previews: PreviewProvider { + static var previews: some View { + LoginView() + } +} From 0f7a1c3ff6600997254e8afc33402bc508e5dacf Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 3 Sep 2024 15:54:27 +0100 Subject: [PATCH 16/29] Styled Login View --- MobileAcebook/LoginView.swift | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index b1be4b99..0570b9b8 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -15,11 +15,11 @@ struct LoginView: View { } @State private var username: String = "" @State private var email: String = "" - @State private var password: String = "" + var body: some View { VStack { - Text("Sign Up!") + Text("Login!") .font( .system(size: 40, weight: .bold, design: .default)) .multilineTextAlignment(.center) @@ -49,20 +49,10 @@ struct LoginView: View { .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) .background(.white.opacity(0.95)) - Spacer() - TextField( - "Enter Password", - text: $password - ) - .padding(.leading, 16) - .padding(.trailing, 0) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) } .padding(0) .padding(.bottom) - .frame(width: 302, height: 242, alignment: .center) + .frame(width: 302, height: 180, alignment: .center) .cornerRadius(10) HStack(alignment: .center, spacing: 3) { Button(action: submit) { Text("Sign Up!") @@ -75,7 +65,7 @@ struct LoginView: View { .background(Constants.ColorsBlue) .cornerRadius(40) - HStack(alignment: .center, spacing: 0) { Text("Already have an account?
Login") + HStack(alignment: .center, spacing: 0) { Text("Don't have an account? \nLogin!") .font(Font.custom("SF Pro", size: 18)) .multilineTextAlignment(.center) .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) From 048271bcc84c2a16bba9a04c35e04bab8dc4a7c6 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:45:27 +0100 Subject: [PATCH 17/29] Added FullPostView and Model as changed post services to async --- MobileAcebook.xcodeproj/project.pbxproj | 12 +- MobileAcebook/FullPostView.swift | 183 +++++++++++++++++++ MobileAcebook/Models/FullPostViewModel.swift | 72 ++++++++ MobileAcebook/Services/PostService.swift | 143 ++++----------- 4 files changed, 304 insertions(+), 106 deletions(-) create mode 100644 MobileAcebook/FullPostView.swift create mode 100644 MobileAcebook/Models/FullPostViewModel.swift diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 3df6d394..21503e2b 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ AE5D85E32AC9AFD2009680C6 /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */; }; AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */; }; AE5D85E82AC9B29A009680C6 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E72AC9B29A009680C6 /* User.swift */; }; + F83545452C875D9300AB9C9E /* FullPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83545442C875D9300AB9C9E /* FullPostViewModel.swift */; }; + F83545472C875DAC00AB9C9E /* FullPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83545462C875DAC00AB9C9E /* FullPostView.swift */; }; F844A8AC2C874802007EA48A /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8A92C874802007EA48A /* PostService.swift */; }; F844A8AD2C874802007EA48A /* CommentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AA2C874802007EA48A /* CommentService.swift */; }; F844A8AE2C874802007EA48A /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8AB2C874802007EA48A /* UserService.swift */; }; @@ -65,6 +67,8 @@ AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; AE5D85E72AC9B29A009680C6 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + F83545442C875D9300AB9C9E /* FullPostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullPostViewModel.swift; sourceTree = ""; }; + F83545462C875DAC00AB9C9E /* FullPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullPostView.swift; sourceTree = ""; }; F844A8A92C874802007EA48A /* PostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = ""; }; F844A8AA2C874802007EA48A /* CommentService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentService.swift; sourceTree = ""; }; F844A8AB2C874802007EA48A /* UserService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; @@ -133,6 +137,7 @@ 25F17D2E2C870CAD001CEF06 /* LoginView.swift */, F844A8B52C874D56007EA48A /* CreatePostView.swift */, F844A8B72C875538007EA48A /* DateExtension.swift */, + F83545462C875DAC00AB9C9E /* FullPostView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -189,6 +194,7 @@ F844A8B02C87480F007EA48A /* Comment.swift */, F844A8AF2C87480F007EA48A /* Post.swift */, AE5D85E72AC9B29A009680C6 /* User.swift */, + F83545442C875D9300AB9C9E /* FullPostViewModel.swift */, ); path = Models; sourceTree = ""; @@ -265,7 +271,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1540; TargetAttributes = { AE5D85AB2AC8A221009680C6 = { CreatedOnToolsVersion = 14.2; @@ -332,6 +338,7 @@ buildActionMask = 2147483647; files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, + F83545472C875DAC00AB9C9E /* FullPostView.swift in Sources */, F844A8B82C875538007EA48A /* DateExtension.swift in Sources */, F844A8B12C87480F007EA48A /* Post.swift in Sources */, F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */, @@ -340,6 +347,7 @@ F844A8AC2C874802007EA48A /* PostService.swift in Sources */, F844A8AD2C874802007EA48A /* CommentService.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, + F83545452C875D9300AB9C9E /* FullPostViewModel.swift in Sources */, F844A8B22C87480F007EA48A /* Comment.swift in Sources */, F844A8AE2C874802007EA48A /* UserService.swift in Sources */, AE5D85E82AC9B29A009680C6 /* User.swift in Sources */, @@ -419,6 +427,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -479,6 +488,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift new file mode 100644 index 00000000..b9777118 --- /dev/null +++ b/MobileAcebook/FullPostView.swift @@ -0,0 +1,183 @@ +// +// FullPostView.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// +import SwiftUI + +struct FullPostView: View { + @StateObject private var viewModel = FullPostViewModel() + let postId: String + let token: String + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + if viewModel.hasError { + mockPostView + } else if let post = viewModel.post { + // Display the image and message... + if let imageUrl = post.imgUrl { + AsyncImage(url: URL(string: imageUrl)) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(10) + } placeholder: { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 200) + .cornerRadius(10) + } + } else { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 200) + .cornerRadius(10) + } + + Text(post.message) + .font(.body) + .padding(.horizontal) + + // Like Button for a real post + likeButton(isMock: false) + + Divider() + + // Comments Section + Text("Comments") + .font(.headline) + .padding(.horizontal) + + if let comments = viewModel.comments { + ForEach(comments) { comment in + VStack(alignment: .leading, spacing: 8) { + Text(comment.createdBy.username) + .font(.caption) + .foregroundColor(.gray) + Text(comment.message) + .font(.body) + Divider() + } + .padding(.horizontal) + } + } else { + Text("No comments yet.") + .padding(.horizontal) + } + } else { + // Loading state (Optional) + Text("Loading...") + .padding(.horizontal) + } + } + } + + // Add Comment Button + HStack { + Spacer() + Button(action: { + // Handle adding a comment (e.g., show a sheet or navigate to a new view) + }) { + Image(systemName: "plus.circle.fill") + .resizable() + .frame(width: 44, height: 44) + .foregroundColor(.blue) + } + Spacer().frame(width: 20) + } + .padding() + } + .navigationBarTitleDisplayMode(.inline) + .onAppear { + viewModel.fetchPost(postId: postId, token: token) + viewModel.fetchComments(postId: postId, token: token) + } + } + + // Mock Post View + private var mockPostView: some View { + VStack(alignment: .leading, spacing: 8) { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 200) + .cornerRadius(10) + .padding(.horizontal) + .padding(.top, 16) + + Text("This is a mock post. The original post could not be loaded.") + .font(.body) + .padding(.horizontal) + + // Like Button for the mock post + likeButton(isMock: true) + + Divider() + .padding(.horizontal) + + // Mock Comments Section + Text("Comments") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Text("This is a mock comment.") + .font(.body) + Divider() + Text("This is another mock comment.") + .font(.body) + } + .padding(.horizontal) + + Spacer() + } + } + + // Like Button logic based on whether it's a mock post or real post + @ViewBuilder + private func likeButton(isMock: Bool) -> some View { + HStack { + Spacer() + Button(action: { + if isMock { + // Toggle like for mock, no server update + viewModel.isLiked.toggle() + } else { + // Toggle like for real post, send server update + viewModel.toggleLike(postId: postId, token: token) + } + }) { + HStack(alignment: .center, spacing: 3) { + Image(systemName: viewModel.isLiked ? "heart.fill" : "heart") + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(viewModel.isLiked ? .red : .black) + Text(viewModel.isLiked ? "Liked" : "Like") + .font(.body) + .foregroundColor(viewModel.isLiked ? .red : .black) + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(height: 44, alignment: .center) // Fixed height to prevent resizing + .background(Color.clear) // Transparent background + .cornerRadius(40) + .overlay( + RoundedRectangle(cornerRadius: 40) + .stroke(viewModel.isLiked ? Color.red : Color.black, lineWidth: 2) // Add border to maintain button shape + ) + } + Spacer().frame(width: 20) + } + .padding(.horizontal) + } +} + struct FullPostView_Previews: PreviewProvider { + static var previews: some View { + FullPostView(postId: "examplePostId", token: "exampleToken") + } + } + + diff --git a/MobileAcebook/Models/FullPostViewModel.swift b/MobileAcebook/Models/FullPostViewModel.swift new file mode 100644 index 00000000..48840242 --- /dev/null +++ b/MobileAcebook/Models/FullPostViewModel.swift @@ -0,0 +1,72 @@ +import Foundation +import Combine + +class FullPostViewModel: ObservableObject { + @Published var post: Post? + @Published var comments: [Comment]? + @Published var isLiked: Bool = false + @Published var hasError: Bool = false // Add this property to track errors + + func fetchPost(postId: String, token: String) { + Task { + do { + let posts = try await PostService.shared.fetchPosts() + if let fetchedPost = posts.first(where: { $0.id == postId }) { + DispatchQueue.main.async { + self.post = fetchedPost + self.isLiked = fetchedPost.likes.contains(token) // Check if user has already liked the post + self.hasError = false // Reset the error state + } + } else { + DispatchQueue.main.async { + self.hasError = true // Set error state if no post found + } + } + } catch { + print("Error fetching post: \(error)") + DispatchQueue.main.async { + self.hasError = true // Set error state if there's an error + } + } + } + } + + func fetchComments(postId: String, token: String) { + // Assuming CommentService is not async yet, but if it is, similar changes as fetchPost can be done. + CommentService.shared.fetchComments(forPostId: postId, token: token) { [weak self] comments, error in + guard let self = self else { return } + if let comments = comments { + DispatchQueue.main.async { + self.comments = comments + } + } else if let error = error { + print("Error fetching comments: \(error)") + } + } + } + + func toggleLike(postId: String, token: String) { + guard post != nil else { return } + + // Toggle the isLiked state locally + isLiked.toggle() + + Task { + do { + let success = try await PostService.shared.updateLikes(postId: postId, token: token) + if !success { + // Revert the isLiked state on error + DispatchQueue.main.async { + self.isLiked.toggle() + } + } + } catch { + print("Error updating likes: \(error)") + // Revert the isLiked state on error + DispatchQueue.main.async { + self.isLiked.toggle() + } + } + } + } +} diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index e6e08463..c284a996 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -1,9 +1,3 @@ -// -// PostService.swift -// MobileAcebook -// -// Created by Sam Quincey on 03/09/2024. -// import UIKit import Foundation @@ -14,91 +8,60 @@ class PostService { private init() {} // Fetch all posts - func fetchPosts(completion: @escaping ([Post]?, Error?) -> Void) { - guard let url = URL(string: "\(baseURL)/posts") else { return } - - let task = URLSession.shared.dataTask(with: url) { data, response, error in - if let error = error { - completion(nil, error) - return - } - - guard let data = data else { - completion(nil, NSError(domain: "DataError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data returned"])) - return - } - - do { - let posts = try JSONDecoder().decode([Post].self, from: data) - completion(posts, nil) - } catch let jsonError { - completion(nil, jsonError) - } + func fetchPosts() async throws -> [Post] { + guard let url = URL(string: "\(baseURL)/posts") else { + throw URLError(.badURL) } - task.resume() + let (data, _) = try await URLSession.shared.data(from: url) + + let posts = try JSONDecoder().decode([Post].self, from: data) + return posts } // Create a new post with optional image - func createPost(message: String, image: UIImage?, token: String, completion: @escaping (Bool, Error?) -> Void) { + func createPost(message: String, image: UIImage?, token: String) async throws -> Bool { if let image = image { // If the user selected an image, upload it to Cloudinary first - uploadImageToCloudinary(image: image) { url, error in - if let url = url { - // After getting the image URL, create the post with the image - self.createPostWithImage(message: message, imgUrl: url, token: token, completion: completion) - } else { - completion(false, error) - } - } + let url = try await uploadImageToCloudinary(image: image) + // After getting the image URL, create the post with the image + return try await createPostWithImage(message: message, imgUrl: url, token: token) } else { // If no image was selected, create the post without an image - self.createPostWithImage(message: message, imgUrl: nil, token: token, completion: completion) + return try await createPostWithImage(message: message, imgUrl: nil, token: token) } } // Helper function to create post with or without image URL - private func createPostWithImage(message: String, imgUrl: String?, token: String, completion: @escaping (Bool, Error?) -> Void) { - guard let url = URL(string: "\(baseURL)/posts") else { return } + private func createPostWithImage(message: String, imgUrl: String?, token: String) async throws -> Bool { + guard let url = URL(string: "\(baseURL)/posts") else { + throw URLError(.badURL) + } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - // Assuming `token` contains the user ID or you have access to the user's ID let body: [String: Any] = [ "message": message, "createdBy": token, // Assuming token is the user ID, replace if necessary "imgUrl": imgUrl ?? NSNull() ] - do { - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - request.httpBody = jsonData - } catch let encodingError { - completion(false, encodingError) - return - } + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - completion(false, error) - return - } - - completion(true, nil) - } + let (_, response) = try await URLSession.shared.data(for: request) - task.resume() + return (response as? HTTPURLResponse)?.statusCode == 200 } // Upload image to Cloudinary - private func uploadImageToCloudinary(image: UIImage, completion: @escaping (String?, Error?) -> Void) { + private func uploadImageToCloudinary(image: UIImage) async throws -> String { guard let cloudName = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_CLOUD_NAME") as? String, let uploadPreset = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_UPLOAD_PRESET") as? String else { - completion(nil, NSError(domain: "CloudinaryError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cloudinary credentials not found."])) - return + throw NSError(domain: "CloudinaryError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cloudinary credentials not found."]) } let url = URL(string: "https://api.cloudinary.com/v1_1/\(cloudName)/image/upload")! @@ -111,12 +74,10 @@ class PostService { var data = Data() - // Add your unsigned Cloudinary preset data.append("--\(boundary)\r\n".data(using: .utf8)!) data.append("Content-Disposition: form-data; name=\"upload_preset\"\r\n\r\n".data(using: .utf8)!) data.append("\(uploadPreset)\r\n".data(using: .utf8)!) - // Add image data if let imageData = image.jpegData(compressionQuality: 0.7) { data.append("--\(boundary)\r\n".data(using: .utf8)!) data.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!) @@ -129,35 +90,21 @@ class PostService { request.httpBody = data - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - completion(nil, error) - return - } - - guard let data = data else { - completion(nil, nil) - return - } - - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let url = json["secure_url"] as? String { - completion(url, nil) - } else { - completion(nil, nil) - } - } catch { - completion(nil, error) - } - } + let (responseData, _) = try await URLSession.shared.data(for: request) - task.resume() + if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any], + let url = json["secure_url"] as? String { + return url + } else { + throw NSError(domain: "CloudinaryError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to upload image."]) + } } - + // Update likes for a post - func updateLikes(postId: String, token: String, completion: @escaping (Bool, Error?) -> Void) { - guard let url = URL(string: "\(baseURL)/posts/\(postId)") else { return } + func updateLikes(postId: String, token: String) async throws -> Bool { + guard let url = URL(string: "\(baseURL)/posts/\(postId)") else { + throw URLError(.badURL) + } var request = URLRequest(url: url) request.httpMethod = "PUT" @@ -166,25 +113,11 @@ class PostService { let body: [String: Any] = ["postId": postId] - do { - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - request.httpBody = jsonData - } catch let encodingError { - completion(false, encodingError) - return - } + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - completion(false, error) - return - } - - completion(true, nil) - } + let (_, response) = try await URLSession.shared.data(for: request) - task.resume() + return (response as? HTTPURLResponse)?.statusCode == 200 } } - - From d5ea2f3097454ea360d3fc4cf5f61a4c75e7155b Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:23:21 +0100 Subject: [PATCH 18/29] Added log out pop up --- .DS_Store | Bin 6148 -> 6148 bytes MobileAcebook.xcodeproj/project.pbxproj | 4 ++ MobileAcebook/LogoutConfirmationView.swift | 59 +++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 MobileAcebook/LogoutConfirmationView.swift diff --git a/.DS_Store b/.DS_Store index 6588252437145a9af8ef5a488082265e7479a1f0..741ddea8fa684bfe5a1c84734be2eb3c837cc1ea 100644 GIT binary patch delta 72 zcmZoMXffEZorQ7d{8X$h87kEll55@8G9xNvdS~|ZZ2e<%CwoC<1aq|J~<80 diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 21503e2b..cfb78da3 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ F844A8B22C87480F007EA48A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B02C87480F007EA48A /* Comment.swift */; }; F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B52C874D56007EA48A /* CreatePostView.swift */; }; F844A8B82C875538007EA48A /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B72C875538007EA48A /* DateExtension.swift */; }; + F8A25B612C884FF6009AE361 /* LogoutConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -76,6 +77,7 @@ F844A8B02C87480F007EA48A /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; F844A8B52C874D56007EA48A /* CreatePostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostView.swift; sourceTree = ""; }; F844A8B72C875538007EA48A /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutConfirmationView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -138,6 +140,7 @@ F844A8B52C874D56007EA48A /* CreatePostView.swift */, F844A8B72C875538007EA48A /* DateExtension.swift */, F83545462C875DAC00AB9C9E /* FullPostView.swift */, + F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -343,6 +346,7 @@ F844A8B12C87480F007EA48A /* Post.swift in Sources */, F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */, 25F17D2F2C870CAD001CEF06 /* LoginView.swift in Sources */, + F8A25B612C884FF6009AE361 /* LogoutConfirmationView.swift in Sources */, AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */, F844A8AC2C874802007EA48A /* PostService.swift in Sources */, F844A8AD2C874802007EA48A /* CommentService.swift in Sources */, diff --git a/MobileAcebook/LogoutConfirmationView.swift b/MobileAcebook/LogoutConfirmationView.swift new file mode 100644 index 00000000..c4dc240d --- /dev/null +++ b/MobileAcebook/LogoutConfirmationView.swift @@ -0,0 +1,59 @@ +// +// LogoutConfirmationView.swift +// MobileAcebook +// +// Created by Sam Quincey on 04/09/2024. +// + +import SwiftUI + +struct LogoutConfirmationView: View { + @Binding var isShowing: Bool + let onLogout: () -> Void + + var body: some View { + VStack { + Text("Are you sure about logging out?") + .font(.headline) + .padding(.top, 20) + + Spacer() + + HStack { + Button(action: { + // Dismiss the pop-up + isShowing = false + }) { + Text("No") + .foregroundColor(.blue) + .padding() + } + + Spacer() + + Button(action: { + // Perform the logout action + onLogout() + }) { + Text("Log me out") + .foregroundColor(.blue) + .padding() + } + } + .padding([.leading, .trailing, .bottom], 20) + } + .frame(width: 300, height: 150) + .background(Color.gray.opacity(0.3)) + .cornerRadius(10) + .shadow(radius: 10) + } +} + +struct LogoutConfirmationView_Previews: PreviewProvider { + @State static var isShowing = true + static var previews: some View { + LogoutConfirmationView(isShowing: $isShowing, onLogout: { + print("Logged out") + }) + } +} From 1a7b4a937bf2151f56dd1ed9ef3b79b0c2ecb73b Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:56:18 +0100 Subject: [PATCH 19/29] Add auth service and fixed post and comment services --- MobileAcebook/LoginView.swift | 14 +- .../Services/AuthenticationService.swift | 156 ++++++++++++++++-- MobileAcebook/Services/CommentService.swift | 26 ++- MobileAcebook/Services/PostService.swift | 28 ++-- 4 files changed, 179 insertions(+), 45 deletions(-) diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index 0570b9b8..d20b9a43 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -13,8 +13,10 @@ struct LoginView: View { print("Submitted") } - @State private var username: String = "" @State private var email: String = "" + @State private var password: String = "" + @State private var errorMessage: String? + @State private var isLoggedIn: Bool = false var body: some View { @@ -30,8 +32,8 @@ struct LoginView: View { VStack { TextField( - "Enter Username", - text: $username + "Enter Email", + text: $email ) .padding(.leading, 16) .padding(.trailing, 0) @@ -40,8 +42,8 @@ struct LoginView: View { .background(.white.opacity(0.95)) .font(Font.custom("SF Pro", size: 17)) Spacer() - TextField( - "Enter Email", + SecureField( + "Enter Password", text: $email ) .padding(.leading, 16) @@ -55,7 +57,7 @@ struct LoginView: View { .frame(width: 302, height: 180, alignment: .center) .cornerRadius(10) HStack(alignment: .center, spacing: 3) { Button(action: submit) { - Text("Sign Up!") + Text("Login!") .font(Font.custom("SF Pro", size: 20)) .foregroundColor(Constants.GraysWhite) } } diff --git a/MobileAcebook/Services/AuthenticationService.swift b/MobileAcebook/Services/AuthenticationService.swift index 61a7e6e9..f2231240 100644 --- a/MobileAcebook/Services/AuthenticationService.swift +++ b/MobileAcebook/Services/AuthenticationService.swift @@ -1,13 +1,143 @@ -//// -//// AuthenticationService.swift -//// MobileAcebook -//// -//// Created by Josué Estévez Fernández on 01/10/2023. -//// -// -//class AuthenticationService: AuthenticationServiceProtocol { -// func signUp(user: User) -> Bool { -// // Logic to call the backend API for signing up -// return true // placeholder -// } -//} +import Foundation + +class AuthenticationService { + static let shared = AuthenticationService() + + private let baseURL = "http://localhost:3000" + + private init() {} + + // "Local Storage" and Authentication frontend + + private let jwtTokenKey = "jwtToken" + + func saveToken(_ token: String) { + UserDefaults.standard.set(token, forKey: jwtTokenKey) + } + + func getToken() -> String? { + return UserDefaults.standard.string(forKey: jwtTokenKey) + } + + func isLoggedIn() -> Bool { + return getToken() != nil + } + + func logout() { + UserDefaults.standard.removeObject(forKey: jwtTokenKey) + } + + // Login + + func login(email: String, password: String, completion: @escaping (Bool, String?) -> Void) { + guard let url = URL(string: "\(baseURL)/authentication") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = [ + "email": email, + "password": password + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch { + completion(false, "Error encoding login details") + return + } + + URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + completion(false, "Login error: \(error.localizedDescription)") + } + return + } + + guard let data = data else { + DispatchQueue.main.async { + completion(false, "No data received") + } + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let token = json["token"] as? String { + self.saveToken(token) + DispatchQueue.main.async { + completion(true, nil) + } + } else { + DispatchQueue.main.async { + completion(false, "Invalid login response") + } + } + } catch { + DispatchQueue.main.async { + completion(false, "Error parsing response") + } + } + }.resume() + } + + // Sign Up + + func signUp(username: String, email: String, password: String, completion: @escaping (Bool, String?) -> Void) { + guard let url = URL(string: "\(baseURL)/users") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = [ + "username": username, + "email": email, + "password": password + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) + request.httpBody = jsonData + } catch { + completion(false, "Error encoding sign-up details") + return + } + + URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + completion(false, "Sign-up error: \(error.localizedDescription)") + } + return + } + + guard let data = data else { + DispatchQueue.main.async { + completion(false, "No data received") + } + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let message = json["message"] as? String, message.contains("created") { + DispatchQueue.main.async { + completion(true, nil) + } + } else { + DispatchQueue.main.async { + completion(false, "Invalid sign-up response") + } + } + } catch { + DispatchQueue.main.async { + completion(false, "Error parsing response") + } + } + }.resume() + } +} diff --git a/MobileAcebook/Services/CommentService.swift b/MobileAcebook/Services/CommentService.swift index 2d6274fb..0bed792e 100644 --- a/MobileAcebook/Services/CommentService.swift +++ b/MobileAcebook/Services/CommentService.swift @@ -1,10 +1,3 @@ -// -// CommentService.swift -// MobileAcebook -// -// Created by Sam Quincey on 03/09/2024. -// - import Foundation class CommentService { @@ -12,14 +5,16 @@ class CommentService { private let baseURL = "http://localhost:3000" private init() {} - + // Fetch comments for a specific post - func fetchComments(forPostId postId: String, token: String, completion: @escaping ([Comment]?, Error?) -> Void) { + func fetchComments(forPostId postId: String, completion: @escaping ([Comment]?, Error?) -> Void) { guard let url = URL(string: "\(baseURL)/comments/\(postId)") else { return } var request = URLRequest(url: url) request.httpMethod = "GET" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token = AuthenticationService.shared.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { @@ -44,19 +39,19 @@ class CommentService { } // Create a new comment for a specific post - func createComment(message: String, forPostId postId: String, token: String, completion: @escaping (Bool, Error?) -> Void) { + func createComment(message: String, forPostId postId: String, completion: @escaping (Bool, Error?) -> Void) { guard let url = URL(string: "\(baseURL)/comments/\(postId)") else { return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token = AuthenticationService.shared.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } let body: [String: Any] = [ "message": message, - "createdBy": token, - "underPost": postId, - "createdAt": Date().iso8601String() // Assuming you have a Date extension for ISO 8601 format + "createdAt": Date().iso8601String() ] do { @@ -79,4 +74,3 @@ class CommentService { task.resume() } } - diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index c284a996..586891fc 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -6,34 +6,39 @@ class PostService { private let baseURL = "http://localhost:3000" private init() {} - + // Fetch all posts func fetchPosts() async throws -> [Post] { guard let url = URL(string: "\(baseURL)/posts") else { throw URLError(.badURL) } - let (data, _) = try await URLSession.shared.data(from: url) + var request = URLRequest(url: url) + if let token = AuthenticationService.shared.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + let (data, _) = try await URLSession.shared.data(for: request) let posts = try JSONDecoder().decode([Post].self, from: data) return posts } // Create a new post with optional image - func createPost(message: String, image: UIImage?, token: String) async throws -> Bool { + func createPost(message: String, image: UIImage?) async throws -> Bool { if let image = image { // If the user selected an image, upload it to Cloudinary first let url = try await uploadImageToCloudinary(image: image) // After getting the image URL, create the post with the image - return try await createPostWithImage(message: message, imgUrl: url, token: token) + return try await createPostWithImage(message: message, imgUrl: url) } else { // If no image was selected, create the post without an image - return try await createPostWithImage(message: message, imgUrl: nil, token: token) + return try await createPostWithImage(message: message, imgUrl: nil) } } // Helper function to create post with or without image URL - private func createPostWithImage(message: String, imgUrl: String?, token: String) async throws -> Bool { + private func createPostWithImage(message: String, imgUrl: String?) async throws -> Bool { guard let url = URL(string: "\(baseURL)/posts") else { throw URLError(.badURL) } @@ -41,11 +46,12 @@ class PostService { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token = AuthenticationService.shared.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } let body: [String: Any] = [ "message": message, - "createdBy": token, // Assuming token is the user ID, replace if necessary "imgUrl": imgUrl ?? NSNull() ] @@ -101,7 +107,7 @@ class PostService { } // Update likes for a post - func updateLikes(postId: String, token: String) async throws -> Bool { + func updateLikes(postId: String) async throws -> Bool { guard let url = URL(string: "\(baseURL)/posts/\(postId)") else { throw URLError(.badURL) } @@ -109,7 +115,9 @@ class PostService { var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token = AuthenticationService.shared.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } let body: [String: Any] = ["postId": postId] From c42bbe54fa9c9fd1f700a4c7fea57ef6f273f14e Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:51:06 +0100 Subject: [PATCH 20/29] Added service functions to Sign up and Login. Amended auth, post and comment services to better align with backend routes and FullPostViewModel to handle comment and post services correctly --- MobileAcebook/LoginView.swift | 162 +++++++++++------- MobileAcebook/Models/FullPostViewModel.swift | 34 ++-- .../Services/AuthenticationService.swift | 72 +++++--- MobileAcebook/Services/PostService.swift | 12 +- MobileAcebook/SignUpView.swift | 110 ++++++------ 5 files changed, 224 insertions(+), 166 deletions(-) diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index d20b9a43..36d83566 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -8,88 +8,118 @@ import Foundation import SwiftUI -struct LoginView: View { - func submit() -> Void { - print("Submitted") - +struct FeedView: View { + var body: some View { + Text("Welcome to the Feed!") + .font(.largeTitle) + .padding() } +} + +struct LoginView: View { @State private var email: String = "" @State private var password: String = "" @State private var errorMessage: String? @State private var isLoggedIn: Bool = false - var body: some View { - - VStack { - Text("Login!") - .font( - .system(size: 40, weight: .bold, design: .default)) - .multilineTextAlignment(.center) - .foregroundColor(.black) + // Submit function for logging in + func submit() { + AuthenticationService.shared.login(email: email, password: password) { success, error in + if success { + // Save JWT and navigate to FeedView + DispatchQueue.main.async { + print("User logged in successfully") + isLoggedIn = true // This triggers the NavigationLink + } + } else { + // Show error message + DispatchQueue.main.async { + errorMessage = error + } + } + } + } - .frame(width: 288, height: 79, alignment: .center) + var body: some View { + NavigationView { VStack { + Text("Login!") + .font(.system(size: 40, weight: .bold, design: .default)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + .frame(width: 288, height: 79, alignment: .center) + VStack { + VStack { + // Email input field + TextField("Enter Email", text: $email) + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + .font(Font.custom("SF Pro", size: 17)) + + // Password input field + SecureField("Enter Password", text: $password) + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(.white.opacity(0.95)) + } + .padding(0) + .padding(.bottom) + .frame(width: 302, height: 180, alignment: .center) + .cornerRadius(10) + + // Show error message if any + if let errorMessage = errorMessage { + Text(errorMessage) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + } - TextField( - "Enter Email", - text: $email - ) - .padding(.leading, 16) - .padding(.trailing, 0) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) - .font(Font.custom("SF Pro", size: 17)) - Spacer() - SecureField( - "Enter Password", - text: $email - ) - .padding(.leading, 16) - .padding(.trailing, 0) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) + // Login button + HStack(alignment: .center, spacing: 3) { + Button(action: submit) { + Text("Login!") + .font(Font.custom("SF Pro", size: 20)) + .foregroundColor(Constants.GraysWhite) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: 113, height: 48, alignment: .center) + .background(Constants.ColorsBlue) + .cornerRadius(40) + + // Don't have an account? Sign up prompt + HStack(alignment: .center, spacing: 0) { + Text("Don't have an account? \nSign up!") + .font(Font.custom("SF Pro", size: 18)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) + .frame(width: 272, height: 43, alignment: .top) + } + .padding(0) + .frame(width: 272, height: 43, alignment: .center) + + // NavigationLink to FeedView, activated when logged in + NavigationLink(destination: FeedView(), isActive: $isLoggedIn) { + EmptyView() // NavigationLink will trigger programmatically + } } - .padding(0) - .padding(.bottom) - .frame(width: 302, height: 180, alignment: .center) - .cornerRadius(10) - HStack(alignment: .center, spacing: 3) { Button(action: submit) { - Text("Login!") - .font(Font.custom("SF Pro", size: 20)) - .foregroundColor(Constants.GraysWhite) - } } - .padding(.horizontal, 10) - .padding(.vertical, 4) - .frame(width: 113, height: 48, alignment: .center) - .background(Constants.ColorsBlue) - .cornerRadius(40) - - HStack(alignment: .center, spacing: 0) { Text("Don't have an account? \nLogin!") - .font(Font.custom("SF Pro", size: 18)) - .multilineTextAlignment(.center) - .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) - .frame(width: 272, height: 43, alignment: .top) } - .padding(0) - .frame(width: 272, height: 43, alignment: .center) + .frame(width: 335, height: 432) + .background(.white.opacity(0.75)) + .cornerRadius(48) } - .frame(width: 335, height: 432) - .background(.white.opacity(0.75)) - - .cornerRadius(48) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0, green: 0.96, blue: 1)) + .statusBar(hidden: false) } - - - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0, green: 0.96, blue: 1)) - .statusBar(hidden: false) } - } - struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() diff --git a/MobileAcebook/Models/FullPostViewModel.swift b/MobileAcebook/Models/FullPostViewModel.swift index 48840242..66d4458c 100644 --- a/MobileAcebook/Models/FullPostViewModel.swift +++ b/MobileAcebook/Models/FullPostViewModel.swift @@ -10,7 +10,7 @@ class FullPostViewModel: ObservableObject { func fetchPost(postId: String, token: String) { Task { do { - let posts = try await PostService.shared.fetchPosts() + let posts = try await PostService.fetchPosts() // Static call if let fetchedPost = posts.first(where: { $0.id == postId }) { DispatchQueue.main.async { self.post = fetchedPost @@ -32,39 +32,41 @@ class FullPostViewModel: ObservableObject { } func fetchComments(postId: String, token: String) { - // Assuming CommentService is not async yet, but if it is, similar changes as fetchPost can be done. - CommentService.shared.fetchComments(forPostId: postId, token: token) { [weak self] comments, error in + CommentService.shared.fetchComments(forPostId: postId) { [weak self] comments, error in guard let self = self else { return } - if let comments = comments { + + if let error = error { DispatchQueue.main.async { - self.comments = comments + print("Error fetching comments: \(error)") + self.hasError = true } - } else if let error = error { - print("Error fetching comments: \(error)") + return + } + + DispatchQueue.main.async { + self.comments = comments ?? [] + self.hasError = false } } } func toggleLike(postId: String, token: String) { guard post != nil else { return } - - // Toggle the isLiked state locally - isLiked.toggle() - + + isLiked.toggle() // Toggle the isLiked state locally + Task { do { - let success = try await PostService.shared.updateLikes(postId: postId, token: token) + let success = try await PostService.updateLikes(postId: postId) // Static call if !success { - // Revert the isLiked state on error DispatchQueue.main.async { - self.isLiked.toggle() + self.isLiked.toggle() // Revert the isLiked state on failure } } } catch { print("Error updating likes: \(error)") - // Revert the isLiked state on error DispatchQueue.main.async { - self.isLiked.toggle() + self.isLiked.toggle() // Revert the isLiked state on error } } } diff --git a/MobileAcebook/Services/AuthenticationService.swift b/MobileAcebook/Services/AuthenticationService.swift index f2231240..d1c67dd2 100644 --- a/MobileAcebook/Services/AuthenticationService.swift +++ b/MobileAcebook/Services/AuthenticationService.swift @@ -27,10 +27,13 @@ class AuthenticationService { UserDefaults.standard.removeObject(forKey: jwtTokenKey) } - // Login + // MARK: - Login func login(email: String, password: String, completion: @escaping (Bool, String?) -> Void) { - guard let url = URL(string: "\(baseURL)/authentication") else { return } + guard let url = URL(string: "\(baseURL)/tokens") else { + completion(false, "Invalid URL") + return + } var request = URLRequest(url: url) request.httpMethod = "POST" @@ -57,37 +60,48 @@ class AuthenticationService { return } - guard let data = data else { + guard let data = data, let httpResponse = response as? HTTPURLResponse else { DispatchQueue.main.async { completion(false, "No data received") } return } - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let token = json["token"] as? String { - self.saveToken(token) - DispatchQueue.main.async { - completion(true, nil) + if (200...299).contains(httpResponse.statusCode) { + // Handle success response + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let token = json["token"] as? String { + self.saveToken(token) + DispatchQueue.main.async { + completion(true, nil) + } + } else { + DispatchQueue.main.async { + completion(false, "Invalid login response") + } } - } else { + } catch { DispatchQueue.main.async { - completion(false, "Invalid login response") + completion(false, "Error parsing response") } } - } catch { + } else { + // Handle HTTP error responses (e.g. 401 Unauthorized) DispatchQueue.main.async { - completion(false, "Error parsing response") + completion(false, "Login failed with status code: \(httpResponse.statusCode)") } } }.resume() } - // Sign Up + // MARK: - Sign Up func signUp(username: String, email: String, password: String, completion: @escaping (Bool, String?) -> Void) { - guard let url = URL(string: "\(baseURL)/users") else { return } + guard let url = URL(string: "\(baseURL)/users") else { + completion(false, "Invalid URL") + return + } var request = URLRequest(url: url) request.httpMethod = "POST" @@ -115,27 +129,35 @@ class AuthenticationService { return } - guard let data = data else { + guard let data = data, let httpResponse = response as? HTTPURLResponse else { DispatchQueue.main.async { completion(false, "No data received") } return } - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let message = json["message"] as? String, message.contains("created") { - DispatchQueue.main.async { - completion(true, nil) + if (200...299).contains(httpResponse.statusCode) { + // Handle success response + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let message = json["message"] as? String, message.contains("created") { + DispatchQueue.main.async { + completion(true, nil) + } + } else { + DispatchQueue.main.async { + completion(false, "Invalid sign-up response") + } } - } else { + } catch { DispatchQueue.main.async { - completion(false, "Invalid sign-up response") + completion(false, "Error parsing response") } } - } catch { + } else { + // Handle HTTP error responses (e.g. 400 Bad Request) DispatchQueue.main.async { - completion(false, "Error parsing response") + completion(false, "Sign-up failed with status code: \(httpResponse.statusCode)") } } }.resume() diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index 586891fc..97e57e15 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -3,12 +3,12 @@ import Foundation class PostService { static let shared = PostService() - private let baseURL = "http://localhost:3000" + private static let baseURL = "http://localhost:3000" private init() {} // Fetch all posts - func fetchPosts() async throws -> [Post] { + static func fetchPosts() async throws -> [Post] { guard let url = URL(string: "\(baseURL)/posts") else { throw URLError(.badURL) } @@ -25,7 +25,7 @@ class PostService { } // Create a new post with optional image - func createPost(message: String, image: UIImage?) async throws -> Bool { + static func createPost(message: String, image: UIImage?) async throws -> Bool { if let image = image { // If the user selected an image, upload it to Cloudinary first let url = try await uploadImageToCloudinary(image: image) @@ -38,7 +38,7 @@ class PostService { } // Helper function to create post with or without image URL - private func createPostWithImage(message: String, imgUrl: String?) async throws -> Bool { + static private func createPostWithImage(message: String, imgUrl: String?) async throws -> Bool { guard let url = URL(string: "\(baseURL)/posts") else { throw URLError(.badURL) } @@ -64,7 +64,7 @@ class PostService { } // Upload image to Cloudinary - private func uploadImageToCloudinary(image: UIImage) async throws -> String { + static private func uploadImageToCloudinary(image: UIImage) async throws -> String { guard let cloudName = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_CLOUD_NAME") as? String, let uploadPreset = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_UPLOAD_PRESET") as? String else { throw NSError(domain: "CloudinaryError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cloudinary credentials not found."]) @@ -107,7 +107,7 @@ class PostService { } // Update likes for a post - func updateLikes(postId: String) async throws -> Bool { + static func updateLikes(postId: String) async throws -> Bool { guard let url = URL(string: "\(baseURL)/posts/\(postId)") else { throw URLError(.badURL) } diff --git a/MobileAcebook/SignUpView.swift b/MobileAcebook/SignUpView.swift index 635077ac..f2a63dab 100644 --- a/MobileAcebook/SignUpView.swift +++ b/MobileAcebook/SignUpView.swift @@ -1,107 +1,111 @@ -// -// SignUpView.swift -// MobileAcebook -// -// Created by William Alexander on 03/09/2024. -// - import Foundation import SwiftUI + struct Constants { - static let ColorsBlue: Color = Color(red: 0, green: 0.48, blue: 1) + static let ColorsBlue: Color = Color(red: 0, green: 0.48, blue: 1) static let GraysWhite: Color = .white } struct SignUpView: View { - func submit() -> Void { - print("Submitted") - - } + // State variables for user input and error handling @State private var username: String = "" @State private var email: String = "" @State private var password: String = "" + @State private var errorMessage: String? + + // Submit function for signing up + func submit() { + AuthenticationService.shared.signUp(username: username, email: email, password: password) { success, error in + if success { + // Handle successful sign-up, such as navigating to a new view + print("User signed up successfully") + } else { + // Show error message + errorMessage = error + } + } + } + var body: some View { - VStack { Text("Sign Up!") - .font( - .system(size: 40, weight: .bold, design: .default)) - .multilineTextAlignment(.center) - .foregroundColor(.black) - - .frame(width: 288, height: 79, alignment: .center) + .font(.system(size: 40, weight: .bold, design: .default)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + .frame(width: 288, height: 79, alignment: .center) + VStack { VStack { - - TextField( - "Enter Username", - text: $username - ) + // Username input + TextField("Enter Username", text: $username) .padding(.leading, 16) - .padding(.trailing, 0) .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) + .background(Color.white.opacity(0.95)) .font(Font.custom("SF Pro", size: 17)) - Spacer() - TextField( - "Enter Email", - text: $email - ) + + // Email input + TextField("Enter Email", text: $email) .padding(.leading, 16) - .padding(.trailing, 0) .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) - Spacer() - TextField( - "Enter Password", - text: $password - ) + .background(Color.white.opacity(0.95)) + + // Password input + SecureField("Enter Password", text: $password) .padding(.leading, 16) - .padding(.trailing, 0) .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) + .background(Color.white.opacity(0.95)) } .padding(0) .padding(.bottom) .frame(width: 302, height: 242, alignment: .center) .cornerRadius(10) - HStack(alignment: .center, spacing: 3) { Button(action: submit) { - Text("Sign Up!") - .font(Font.custom("SF Pro", size: 20)) - .foregroundColor(Constants.GraysWhite) - } } + + // Show error message if any + if let errorMessage = errorMessage { + Text(errorMessage) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + } + + // Sign Up button + HStack(alignment: .center, spacing: 3) { + Button(action: submit) { + Text("Sign Up!") + .font(Font.custom("SF Pro", size: 20)) + .foregroundColor(Constants.GraysWhite) + } + } .padding(.horizontal, 10) .padding(.vertical, 4) .frame(width: 113, height: 48, alignment: .center) .background(Constants.ColorsBlue) .cornerRadius(40) - HStack(alignment: .center, spacing: 0) { Text("Already have an account?
Login") + // Already have an account? Login prompt + HStack(alignment: .center, spacing: 0) { + Text("Already have an account? Login") .font(Font.custom("SF Pro", size: 18)) .multilineTextAlignment(.center) .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) - .frame(width: 272, height: 43, alignment: .top) } + .frame(width: 272, height: 43, alignment: .top) + } .padding(0) .frame(width: 272, height: 43, alignment: .center) } .frame(width: 335, height: 432) - .background(.white.opacity(0.75)) - + .background(Color.white.opacity(0.75)) .cornerRadius(48) } - - .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(red: 0, green: 0.96, blue: 1)) .statusBar(hidden: false) } - } - struct SignUpView_Previews: PreviewProvider { static var previews: some View { SignUpView() From 490bcfdee5c9c476ad387c43ced3b865ee42e345 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:13:07 +0100 Subject: [PATCH 21/29] Sign in and LOgin functioning --- .DS_Store | Bin 6148 -> 6148 bytes MobileAcebook.xcodeproj/project.pbxproj | 8 ++ MobileAcebook/.DS_Store | Bin 8196 -> 10244 bytes MobileAcebook/Assets.xcassets/.DS_Store | Bin 0 -> 6148 bytes MobileAcebook/FeedView.swift | 62 ++++++++++++ MobileAcebook/FullPostView.swift | 4 +- MobileAcebook/LoginView.swift | 8 -- MobileAcebook/PostCardView.swift | 89 ++++++++++++++++++ .../Services/AuthenticationService.swift | 41 +++++++- MobileAcebook/Services/PostService.swift | 48 ++++++++-- 10 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 MobileAcebook/Assets.xcassets/.DS_Store create mode 100644 MobileAcebook/FeedView.swift create mode 100644 MobileAcebook/PostCardView.swift diff --git a/.DS_Store b/.DS_Store index 741ddea8fa684bfe5a1c84734be2eb3c837cc1ea..8697ea9b23a634797c5784a650dd94c76dbdd67b 100644 GIT binary patch delta 75 zcmZoMXffEJ!^&xBY@wrIXliUQ*_8D&Gt+`YlMC5QCdadJvE9;L;;Qd5IiJk|!8Dot Wl8uk;0|O8UO?G6L-YmiXMgRa#kro{Q delta 75 zcmZoMXffEJ!^&xFXrZHEXl!aZ*_8D&GvmhhlMC5QCdadJv3cy6zHG7cUMQEJzhh+HCakd@5e#HCMC z&s9#P5p(Arq<}tAi$YqY&$LU`RBL;~60ig;0ZYIVumt`G0{CXDn-cTH326U`xm)vP4K0;( ztsyV|;e=AAcC8a08||O9W8Vu~g~Bf}GCFo+e8QP`XE z^mwIkw&0#VTP`fPt4~(X&T`J&gGbNb>>T<>f&9t?Az}6wx~1#A>$mJ-o>ZEFFN1xw zR95DJsSTV5fEqx0d?cczM12ECBrSdtnmdcY-$8kgS_#}gU~npedzQ-7gl?5BY{FiM z(iU_ZuyCQB*tFfIbz|cn-bVb!*uF>JW{O^*-v-q$_Xe?%%s{tIK7N-T#fah??SGnH zfxtdSA7sCQm``Zu`)Gd|%rMpaeWNdVuFE{Pdw7>79Wh%y+u3simx__OL$6`021`EJ zu3_yoP?Pu_qIQhmzIojrZ zRISM)ls2(x>_cW+B7DVT-z`*avIHyvOTZGa1g?j`sP5KP=l>gn|Np<9m$SWE0+zs) z5D;VQmGu(3IzRtP_Nva>UF1*5Dyeo|Em;K@K8}av<9Jlmar{GM)wZCdN+=hN>}rWS rNLT)6!1*zqjsLIg^z_0*l0=-=C*+NvT-o?Pdf1!_d*iVB$Nzr;g0?hu delta 200 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMA$hR?IH$NlaWJ5uv$$A3vllKUE zOg<%KH+g~Z1jb#H4MhrhrK+oqj7$x56by~cjVG@Yk!I|fEGQ_?*gN@^i2URtG5N_k zqVkh3i3v~M3}l}d_aUh7s>G_zaXc%T1(|^^0RjnbAmIvf)W*W^%#-<50y!8VPG#5} I&ohS^0E;RyfB*mh diff --git a/MobileAcebook/Assets.xcassets/.DS_Store b/MobileAcebook/Assets.xcassets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..66338794289c0105a09b4357343afa8f08d4e259 GIT binary patch literal 6148 zcmeHK!EVz)5S?uUb*L&uC`fQXvcxrngcbzi;-=-$D@Jet6znDnrmi=#9dd{w`40cW znIGYI;LYq-O4=ZW3q;k9HT!mVcGlkK@s5{>RBtlq5;cg(gEH1`p;#eoXT2g@wug;M z=E$g|Bhr*Yoj3*@1DnnOf4faOq*Gi&TKs-p@3{3^>omToNAa6v zIPEu@M>;E`Bpc@9kPLxTv?#*W1{bwz|`((c}Z~3$5 zk2@{@;OW79Uia=ic=X~`|13Sv^jBFDDQs*adloO?8!BrP_a)4;RA(dj@O-yH-5A%2 z5S)SNNZN^QuOj<9{1(YxYmVze^jeF(MDm_wB93Si*9B<)j5yYmEyuCCe~H>Ao#Ta^ z!H?tqp=7fW{MwX=WdzHLSEM*(7GDvX8!a}pz1@v|O80P%0ms1qV1V}rAIcb5tPIN5 zfkKV|z#6(mQ0HF*_VFzS7Au3WK$LL>8dqhH7|OV#-?!reiq9?6 pS$JG!P+U;hbSxE4#do11u=!j81{N!W=z+K&0inSaj)8y5z;6ol-|YYZ literal 0 HcmV?d00001 diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift new file mode 100644 index 00000000..22728fbc --- /dev/null +++ b/MobileAcebook/FeedView.swift @@ -0,0 +1,62 @@ +// +// FeedView.swift +// MobileAcebook +// +// Created by Sam Quincey on 04/09/2024. +// + +import SwiftUI + +struct FeedView: View { + @State private var posts: [Post] = [] + @State private var isLoading: Bool = true + @State private var errorMessage: String? + + var body: some View { + NavigationView { + VStack { + if isLoading { + ProgressView("Loading posts...") + } else if let errorMessage = errorMessage { + Text("Error: \(errorMessage)") + .foregroundColor(.red) + .padding() + } else if posts.isEmpty { + Text("No posts available.") + .padding() + } else { + ScrollView { + ForEach(posts) { post in + PostCardView(post: post, userId: AuthenticationService.shared.getUserId() ?? "") + .padding(.bottom, 10) + } + .padding(.horizontal) + } + } + } + .navigationTitle("Feed") + .onAppear { + fetchPosts() + } + } + } + + private func fetchPosts() { + Task { + do { + let fetchedPosts = try await PostService.fetchPosts() + DispatchQueue.main.async { + self.posts = fetchedPosts + self.isLoading = false + } + } catch { + DispatchQueue.main.async { + self.errorMessage = "Failed to load posts." + self.isLoading = false + } + } + } + } +} + + diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift index b9777118..e56d9981 100644 --- a/MobileAcebook/FullPostView.swift +++ b/MobileAcebook/FullPostView.swift @@ -155,7 +155,9 @@ struct FullPostView: View { .resizable() .frame(width: 24, height: 24) .foregroundColor(viewModel.isLiked ? .red : .black) - Text(viewModel.isLiked ? "Liked" : "Like") + + // Show the number of likes + Text("\(viewModel.post?.likes.count ?? 0)") .font(.body) .foregroundColor(viewModel.isLiked ? .red : .black) } diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index 36d83566..b7c79735 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -8,14 +8,6 @@ import Foundation import SwiftUI -struct FeedView: View { - var body: some View { - Text("Welcome to the Feed!") - .font(.largeTitle) - .padding() - } -} - struct LoginView: View { @State private var email: String = "" @State private var password: String = "" diff --git a/MobileAcebook/PostCardView.swift b/MobileAcebook/PostCardView.swift new file mode 100644 index 00000000..f35e3267 --- /dev/null +++ b/MobileAcebook/PostCardView.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct PostCardView: View { + let post: Post + let userId: String // This is the logged-in user's ID + + @State private var isLiked: Bool + @State private var likesCount: Int + + init(post: Post, userId: String) { + self.post = post + self.userId = userId + _isLiked = State(initialValue: post.likes.contains(userId)) + _likesCount = State(initialValue: post.likes.count) + } + + var body: some View { + VStack(alignment: .leading) { + // Display image (if any) + if let imgUrl = post.imgUrl, let url = URL(string: imgUrl) { + AsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .cornerRadius(10) + } placeholder: { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 150) + .cornerRadius(10) + } + } + + // Display message + Text(post.message) + .lineLimit(3) // Limit the message to prevent the card from being too big + .truncationMode(.tail) + .padding(.vertical, 10) + + HStack { + // Like button and count + Button(action: toggleLike) { + HStack { + Image(systemName: isLiked ? "heart.fill" : "heart") + .foregroundColor(isLiked ? .red : .black) + Text("\(likesCount)") // Display the number of likes + } + } + Spacer() + // Show when the post was created + Text(formatDate(post.createdAt)) + .font(.footnote) + .foregroundColor(.gray) + } + } + .padding() + .background(Color.white) + .cornerRadius(12) + .shadow(radius: 3) + } + + // Handle like toggling + private func toggleLike() { + Task { + do { + let success = try await PostService.updateLikes(postId: post.id) + if success { + isLiked.toggle() + likesCount += isLiked ? 1 : -1 + } + } catch { + print("Error updating likes: \(error)") + } + } + } + + // Helper function to format date string + private func formatDate(_ dateString: String) -> String { + let formatter = ISO8601DateFormatter() + if let date = formatter.date(from: dateString) { + let displayFormatter = DateFormatter() + displayFormatter.dateStyle = .medium + displayFormatter.timeStyle = .short + return displayFormatter.string(from: date) + } + return dateString + } +} diff --git a/MobileAcebook/Services/AuthenticationService.swift b/MobileAcebook/Services/AuthenticationService.swift index d1c67dd2..4cf0bc0a 100644 --- a/MobileAcebook/Services/AuthenticationService.swift +++ b/MobileAcebook/Services/AuthenticationService.swift @@ -1,3 +1,10 @@ +// +// AuthenticationService.swift +// MobileAcebook +// +// Created by Sam Quincey on 03/09/2024. +// + import Foundation class AuthenticationService { @@ -8,27 +15,54 @@ class AuthenticationService { private init() {} // "Local Storage" and Authentication frontend - private let jwtTokenKey = "jwtToken" + // Save token in UserDefaults func saveToken(_ token: String) { UserDefaults.standard.set(token, forKey: jwtTokenKey) } + // Retrieve token from UserDefaults func getToken() -> String? { return UserDefaults.standard.string(forKey: jwtTokenKey) } + // Check if the user is logged in based on the token func isLoggedIn() -> Bool { return getToken() != nil } + // Log out the user by removing the token func logout() { UserDefaults.standard.removeObject(forKey: jwtTokenKey) } - + + // MARK: - JWT Decoding Helper + + // Decode the JWT token to extract payload + func decodeJWT(_ token: String) -> [String: Any]? { + let segments = token.split(separator: ".") + guard segments.count == 3 else { return nil } + + let base64String = String(segments[1]) + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + guard let decodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else { + return nil + } + + return try? JSONSerialization.jsonObject(with: decodedData, options: []) as? [String: Any] + } + + // Retrieve the user ID from the JWT token payload + func getUserId() -> String? { + guard let token = getToken(), let payload = decodeJWT(token) else { return nil } + return payload["user_id"] as? String // Adjust this key based on your JWT structure + } + // MARK: - Login - + func login(email: String, password: String, completion: @escaping (Bool, String?) -> Void) { guard let url = URL(string: "\(baseURL)/tokens") else { completion(false, "Invalid URL") @@ -163,3 +197,4 @@ class AuthenticationService { }.resume() } } + diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index 97e57e15..5dae725d 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -8,7 +8,7 @@ class PostService { private init() {} // Fetch all posts - static func fetchPosts() async throws -> [Post] { + static func fetchPosts() async throws -> [Post] { guard let url = URL(string: "\(baseURL)/posts") else { throw URLError(.badURL) } @@ -18,12 +18,28 @@ class PostService { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } - let (data, _) = try await URLSession.shared.data(for: request) - - let posts = try JSONDecoder().decode([Post].self, from: data) - return posts + do { + let (data, response) = try await URLSession.shared.data(for: request) + + if let httpResponse = response as? HTTPURLResponse { + if httpResponse.statusCode == 200 { + let posts = try JSONDecoder().decode([Post].self, from: data) + return posts + } else { + // Handle non-200 responses + let errorMessage = "Failed to fetch posts: HTTP \(httpResponse.statusCode)" + print(errorMessage) + throw NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]) + } + } else { + throw NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"]) + } + } catch { + print("Error fetching posts: \(error)") + throw error + } } - + // Create a new post with optional image static func createPost(message: String, image: UIImage?) async throws -> Bool { if let image = image { @@ -58,9 +74,17 @@ class PostService { let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) request.httpBody = jsonData - let (_, response) = try await URLSession.shared.data(for: request) - - return (response as? HTTPURLResponse)?.statusCode == 200 + do { + let (_, response) = try await URLSession.shared.data(for: request) + if let httpResponse = response as? HTTPURLResponse { + return httpResponse.statusCode == 200 + } else { + throw NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"]) + } + } catch { + print("Error creating post: \(error)") + throw error + } } // Upload image to Cloudinary @@ -126,6 +150,10 @@ class PostService { let (_, response) = try await URLSession.shared.data(for: request) - return (response as? HTTPURLResponse)?.statusCode == 200 + if let httpResponse = response as? HTTPURLResponse { + return httpResponse.statusCode == 200 + } else { + throw NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"]) + } } } From b4596aae7ac95b241142072b898b926944d6a9d9 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Wed, 4 Sep 2024 23:05:01 +0100 Subject: [PATCH 22/29] Fixed navigation issues --- MobileAcebook.xcodeproj/project.pbxproj | 4 + MobileAcebook/.DS_Store | Bin 10244 -> 10244 bytes MobileAcebook/CreatePostView.swift | 123 +++++++++---- MobileAcebook/FeedView.swift | 103 +++++++---- MobileAcebook/LoginView.swift | 158 ++++++++--------- MobileAcebook/LogoutConfirmationView.swift | 11 +- MobileAcebook/MainView.swift | 96 ++++++++++ MobileAcebook/Models/Post.swift | 14 +- MobileAcebook/Models/User.swift | 7 + MobileAcebook/Services/PostService.swift | 18 +- MobileAcebook/SignUpView.swift | 193 ++++++++++++--------- MobileAcebook/WelcomePageView.swift | 74 ++++---- 12 files changed, 520 insertions(+), 281 deletions(-) create mode 100644 MobileAcebook/MainView.swift diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index bf1aaf21..351742ae 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ F844A8B22C87480F007EA48A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B02C87480F007EA48A /* Comment.swift */; }; F844A8B62C874D56007EA48A /* CreatePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B52C874D56007EA48A /* CreatePostView.swift */; }; F844A8B82C875538007EA48A /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F844A8B72C875538007EA48A /* DateExtension.swift */; }; + F87BD8272C88AF5E0071F4D3 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87BD8262C88AF5E0071F4D3 /* MainView.swift */; }; F8A25B612C884FF6009AE361 /* LogoutConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */; }; /* End PBXBuildFile section */ @@ -81,6 +82,7 @@ F844A8B02C87480F007EA48A /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; F844A8B52C874D56007EA48A /* CreatePostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostView.swift; sourceTree = ""; }; F844A8B72C875538007EA48A /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + F87BD8262C88AF5E0071F4D3 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutConfirmationView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -147,6 +149,7 @@ F8A25B602C884FF6009AE361 /* LogoutConfirmationView.swift */, F8304C5C2C888BF000B4BBC9 /* FeedView.swift */, F8304C5E2C888C0500B4BBC9 /* PostCardView.swift */, + F87BD8262C88AF5E0071F4D3 /* MainView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -359,6 +362,7 @@ F844A8AD2C874802007EA48A /* CommentService.swift in Sources */, F8304C5D2C888BF000B4BBC9 /* FeedView.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, + F87BD8272C88AF5E0071F4D3 /* MainView.swift in Sources */, F83545452C875D9300AB9C9E /* FullPostViewModel.swift in Sources */, F844A8B22C87480F007EA48A /* Comment.swift in Sources */, F844A8AE2C874802007EA48A /* UserService.swift in Sources */, diff --git a/MobileAcebook/.DS_Store b/MobileAcebook/.DS_Store index 7b30b1662955f72322daae4f6ca68cf245dbb9f0..c9824d14f242ba1b5d3ebff4955f2a92acaee66e 100644 GIT binary patch delta 46 zcmV+}0MY-1P=rvBPXQURP`eKS8Iv&)BC{?KGy$``5ljKINE5#dk${@B2O#g6o9#^K52U_{Fk0U4)q#0KjG$K>z>% diff --git a/MobileAcebook/CreatePostView.swift b/MobileAcebook/CreatePostView.swift index adb92d4b..3b980fff 100644 --- a/MobileAcebook/CreatePostView.swift +++ b/MobileAcebook/CreatePostView.swift @@ -1,46 +1,101 @@ -// -// CreatePostView.swift -// MobileAcebook -// -// Created by Maz on 03/09/2024. -// - import SwiftUI struct CreatePostView: View { @State private var userInput: String = "" + @State private var showAlert: Bool = false + @State private var alertTitle: String = "" + @State private var alertMessage: String = "" + @Environment(\.presentationMode) var presentationMode // Handle modal dismissal + var body: some View { - VStack(alignment: .center){ - Text("Make a Post").font(.largeTitle).bold() + VStack(alignment: .center) { + HStack { + // Cancel Button to dismiss CreatePostView + Button(action: { + self.presentationMode.wrappedValue.dismiss() // Dismiss the view + }) { + Text("Cancel") + .foregroundColor(.blue) + } + .padding(.leading, 20) + Spacer() + } + .padding(.top, 20) + + Spacer() + + Text("Make a Post") + .font(.largeTitle) + .bold() + .padding(.bottom, 20) + + // Post Text Field - Centered TextField( "Post text, lorem ipsum day...", text: $userInput, axis: .vertical - ).textFieldStyle(.roundedBorder) - .lineLimit(10, reservesSpace: true) - .multilineTextAlignment(.leading) - .frame(minWidth: 100, maxWidth: 400, minHeight: 100, maxHeight: 250) -// .cornerRadius(40) - HStack(alignment: .center, spacing: 3){ - Button("Add Image"){} - .frame(width: 96, height: 64) - .background(Color(red: 0, green: 0.48, blue: 1)) - .cornerRadius(40) - .foregroundColor(.white) - - Spacer() - Button("Create Post"){} - .frame(width: 96, height: 64) - .background(Color(red: 0, green: 0.48, blue: 1)) - .cornerRadius(40) - .foregroundColor(.white) - - }.padding(40) - - }.frame(maxHeight: 900) - .padding() - .background(Color(red: 0, green: 0.96, blue: 1)) - + ) + .textFieldStyle(.roundedBorder) + .lineLimit(10, reservesSpace: true) + .multilineTextAlignment(.leading) + .frame(minWidth: 100, maxWidth: 400, minHeight: 100, maxHeight: 250) + .padding(.horizontal, 20) + + // Action Buttons - Centered + HStack(alignment: .center, spacing: 20) { + Button("Add Image") { + // Add Image action if necessary + } + .frame(width: 120, height: 44) + .background(Color.blue) + .cornerRadius(40) + .foregroundColor(.white) + + Button("Create Post") { + // Create Post action + Task { + do { + // The token is automatically handled in PostService, so no need to pass it manually + _ = try await PostService.createPost(message: userInput, image: nil) + + // Show success alert + alertTitle = "Post Created" + alertMessage = "Your post has been created successfully." + showAlert = true + + } catch { + // Show error alert + alertTitle = "Error" + alertMessage = "Failed to create the post. Please try again." + showAlert = true + } + } + } + .frame(width: 120, height: 44) + .background(Color.blue) + .cornerRadius(40) + .foregroundColor(.white) + } + .padding(.top, 30) + + Spacer() + + // Alert for showing success or error message + .alert(isPresented: $showAlert) { + Alert( + title: Text(alertTitle), + message: Text(alertMessage), + dismissButton: .default(Text("OK"), action: { + if alertTitle == "Post Created" { + // Dismiss the CreatePostView modal and return to MainView + self.presentationMode.wrappedValue.dismiss() + } + }) + ) + } + } + .background(Color(red: 0, green: 0.96, blue: 1).ignoresSafeArea()) // Cover entire screen with background color + .navigationBarHidden(true) // Hide default navigation bar } } diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift index 22728fbc..33a51bf0 100644 --- a/MobileAcebook/FeedView.swift +++ b/MobileAcebook/FeedView.swift @@ -1,44 +1,37 @@ -// -// FeedView.swift -// MobileAcebook -// -// Created by Sam Quincey on 04/09/2024. -// - import SwiftUI struct FeedView: View { - @State private var posts: [Post] = [] - @State private var isLoading: Bool = true - @State private var errorMessage: String? + @State private var posts: [Post] = [] // To store the fetched posts + @State private var isLoading: Bool = true // To show loading state + @State private var errorMessage: String? // To handle and show errors var body: some View { - NavigationView { - VStack { - if isLoading { - ProgressView("Loading posts...") - } else if let errorMessage = errorMessage { - Text("Error: \(errorMessage)") - .foregroundColor(.red) - .padding() - } else if posts.isEmpty { - Text("No posts available.") - .padding() - } else { - ScrollView { - ForEach(posts) { post in - PostCardView(post: post, userId: AuthenticationService.shared.getUserId() ?? "") - .padding(.bottom, 10) - } - .padding(.horizontal) + VStack { + if isLoading { + ProgressView("Loading posts...") // Show loading indicator + } else if let errorMessage = errorMessage { + Text("Error: \(errorMessage)") + .foregroundColor(.red) + .padding() + } else if posts.isEmpty { + Text("No posts available.") + .padding() + } else { + ScrollView { + // Display the posts using PostView + ForEach(posts) { post in + PostView(post: post) + .padding(.bottom, 10) } + .padding(.horizontal) } } - .navigationTitle("Feed") - .onAppear { - fetchPosts() - } } + .onAppear { + fetchPosts() // Fetch posts when the view appears + } + .background(Color(red: 0, green: 0.48, blue: 1).opacity(0.28)) + .frame(maxWidth: .infinity, maxHeight: .infinity) } private func fetchPosts() { @@ -46,6 +39,7 @@ struct FeedView: View { do { let fetchedPosts = try await PostService.fetchPosts() DispatchQueue.main.async { + print(fetchedPosts) // Check if posts are being received self.posts = fetchedPosts self.isLoading = false } @@ -53,10 +47,55 @@ struct FeedView: View { DispatchQueue.main.async { self.errorMessage = "Failed to load posts." self.isLoading = false + print("Error fetching posts: \(error.localizedDescription)") } } } } + +} + +#Preview { + FeedView() } +struct PostView: View { + let post: Post + + var body: some View { + ZStack { + Rectangle() + .foregroundColor(.clear) + .frame(width: 192, height: 217) + .background(Color(red: 0.85, green: 0.85, blue: 0.85)) + .cornerRadius(48) + .padding(.trailing, 140) + Text("\(post.message)") + .font(Font.custom("SF Pro", size: 17)) + .foregroundColor(.black) + .frame(width: 135, height: 137, alignment: .topLeading) + .padding(.leading, 200) + Image(systemName: checkIfLiked(userId: post.id, post: post) ? "heart.fill" : "heart") + .resizable() + .frame(width: 35, height: 35) + .foregroundColor(checkIfLiked(userId: post.id, post: post) ? .red : .black) + .padding(.top, 200) + .padding(.leading, 200) + } + .frame(width: 393, height: 259) + .background(.white) + .cornerRadius(48) + } +} + +// Helper function to check if a post is liked +func checkIfLiked(userId: String, post: Post) -> Bool { + return post.likes.contains(userId) +} + +struct FeedView_Previews: PreviewProvider { + static var previews: some View { + FeedView() + } +} diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index b7c79735..881fcdab 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -1,11 +1,3 @@ -// -// LoginView.swift -// MobileAcebook -// -// Created by William Alexander on 03/09/2024. -// - -import Foundation import SwiftUI struct LoginView: View { @@ -13,18 +5,16 @@ struct LoginView: View { @State private var password: String = "" @State private var errorMessage: String? @State private var isLoggedIn: Bool = false + @State private var isSignUpViewPresented: Bool = false // State to trigger SignUpView presentation - // Submit function for logging in func submit() { - AuthenticationService.shared.login(email: email, password: password) { success, error in + AuthenticationService.shared.login(email: email.lowercased(), password: password) { success, error in if success { - // Save JWT and navigate to FeedView DispatchQueue.main.async { print("User logged in successfully") - isLoggedIn = true // This triggers the NavigationLink + isLoggedIn = true } } else { - // Show error message DispatchQueue.main.async { errorMessage = error } @@ -33,82 +23,84 @@ struct LoginView: View { } var body: some View { - NavigationView { + VStack { + Text("Login!") + .font(.system(size: 40, weight: .bold)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + .frame(width: 288, height: 79, alignment: .center) + VStack { - Text("Login!") - .font(.system(size: 40, weight: .bold, design: .default)) - .multilineTextAlignment(.center) - .foregroundColor(.black) - .frame(width: 288, height: 79, alignment: .center) - VStack { - VStack { - // Email input field - TextField("Enter Email", text: $email) - .padding(.leading, 16) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) - .font(Font.custom("SF Pro", size: 17)) - - // Password input field - SecureField("Enter Password", text: $password) - .padding(.leading, 16) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(.white.opacity(0.95)) - } - .padding(0) - .padding(.bottom) - .frame(width: 302, height: 180, alignment: .center) - .cornerRadius(10) - - // Show error message if any - if let errorMessage = errorMessage { - Text(errorMessage) - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding() - } - - // Login button - HStack(alignment: .center, spacing: 3) { - Button(action: submit) { - Text("Login!") - .font(Font.custom("SF Pro", size: 20)) - .foregroundColor(Constants.GraysWhite) + // Email input field + TextField("Enter Email", text: $email) + .onChange(of: email) { newValue in + email = newValue.lowercased() } - } - .padding(.horizontal, 10) - .padding(.vertical, 4) - .frame(width: 113, height: 48, alignment: .center) - .background(Constants.ColorsBlue) - .cornerRadius(40) - - // Don't have an account? Sign up prompt - HStack(alignment: .center, spacing: 0) { - Text("Don't have an account? \nSign up!") - .font(Font.custom("SF Pro", size: 18)) - .multilineTextAlignment(.center) - .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) - .frame(width: 272, height: 43, alignment: .top) - } - .padding(0) - .frame(width: 272, height: 43, alignment: .center) - - // NavigationLink to FeedView, activated when logged in - NavigationLink(destination: FeedView(), isActive: $isLoggedIn) { - EmptyView() // NavigationLink will trigger programmatically - } + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(Color.white.opacity(0.95)) + .font(.system(size: 17)) + + // Password input field + SecureField("Enter Password", text: $password) + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(Color.white.opacity(0.95)) + } + .frame(width: 302, height: 180) + .cornerRadius(10) + + // Show error message if any + if let errorMessage = errorMessage { + Text(errorMessage) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + } + + // Login button + Button(action: submit) { + Text("Login!") + .font(.system(size: 20)) + .foregroundColor(.white) + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: 113, height: 48) + .background(Color.blue) + .cornerRadius(40) + + // Sign-up prompt + Button(action: { + isSignUpViewPresented = true + }) { + Text("Don't have an account? Sign up!") + .font(.system(size: 18)) + .multilineTextAlignment(.center) + .foregroundColor(Color.blue) + } + .padding(.top, 10) + + // Navigation to MainView after login + NavigationLink(destination: MainView(), isActive: $isLoggedIn) { + EmptyView() + } + + // Navigation to SignUpView + NavigationLink(destination: SignUpView(), isActive: $isSignUpViewPresented) { + EmptyView() } - .frame(width: 335, height: 432) - .background(.white.opacity(0.75)) - .cornerRadius(48) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0, green: 0.96, blue: 1)) - .statusBar(hidden: false) + .frame(width: 335, height: 432) + .background(Color.white.opacity(0.75)) + .cornerRadius(48) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0, green: 0.96, blue: 1)) + .navigationBarBackButtonHidden(true) } } diff --git a/MobileAcebook/LogoutConfirmationView.swift b/MobileAcebook/LogoutConfirmationView.swift index c4dc240d..ed5562e4 100644 --- a/MobileAcebook/LogoutConfirmationView.swift +++ b/MobileAcebook/LogoutConfirmationView.swift @@ -1,10 +1,3 @@ -// -// LogoutConfirmationView.swift -// MobileAcebook -// -// Created by Sam Quincey on 04/09/2024. -// - import SwiftUI struct LogoutConfirmationView: View { @@ -33,7 +26,7 @@ struct LogoutConfirmationView: View { Button(action: { // Perform the logout action - onLogout() + onLogout() // Log out the user and navigate back }) { Text("Log me out") .foregroundColor(.blue) @@ -43,7 +36,7 @@ struct LogoutConfirmationView: View { .padding([.leading, .trailing, .bottom], 20) } .frame(width: 300, height: 150) - .background(Color.gray.opacity(0.3)) + .background(Color.white.opacity(0.85)) .cornerRadius(10) .shadow(radius: 10) } diff --git a/MobileAcebook/MainView.swift b/MobileAcebook/MainView.swift new file mode 100644 index 00000000..f50cc76a --- /dev/null +++ b/MobileAcebook/MainView.swift @@ -0,0 +1,96 @@ +import SwiftUI + +struct MainView: View { + @State private var isLogoutPopupShowing = false // Control logout pop-up visibility + @State private var showCreatePostView = false // Control showing the Create Post view + @State private var navigateToWelcome = false // Handle navigation to WelcomePageView after logout + + init() { + // Configure tab bar appearance + let tabBarAppearance = UITabBarAppearance() + tabBarAppearance.backgroundColor = UIColor.white + tabBarAppearance.stackedLayoutAppearance.normal.iconColor = UIColor.systemBlue + tabBarAppearance.stackedLayoutAppearance.selected.iconColor = UIColor.systemBlue + + UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance + UITabBar.appearance().standardAppearance = tabBarAppearance + } + + var body: some View { + ZStack { + // Show Feed by default + FeedView() + + VStack { + Spacer() // Pushes the tab bar to the bottom + + // TabView-style bar + HStack { + Spacer() + + // Logout Button (Triggers logout popup) + Button(action: { + isLogoutPopupShowing = true + }) { + VStack { + Image(systemName: "person.slash.fill") + Text("Logout") + } + } + Spacer() + + // Create Post Button (Navigates to Create Post view using fullScreenCover) + Button(action: { + showCreatePostView = true + }) { + VStack { + Image(systemName: "plus.circle.fill") + Text("Create Post") + } + } + Spacer() + + // Refresh Button (Placeholder action) + Button(action: { + print("Refreshing feed...") + }) { + VStack { + Image(systemName: "arrow.clockwise") + Text("Refresh") + } + } + Spacer() + } + .padding() + .background(Color.white) + } + + // Show logout confirmation popup + if isLogoutPopupShowing { + LogoutConfirmationView(isShowing: $isLogoutPopupShowing, onLogout: { + AuthenticationService.shared.logout() // Perform logout + navigateToWelcome = true // Navigate to WelcomePageView + }) + .transition(.opacity) + .animation(.easeInOut, value: isLogoutPopupShowing) + } + } + // Present CreatePostView in a full screen mode without NavigationView + .fullScreenCover(isPresented: $showCreatePostView) { + CreatePostView() + } + // Navigate to the Welcome screen after logout + .fullScreenCover(isPresented: $navigateToWelcome) { + WelcomePageView() // Assume WelcomePageView exists + } + // Ensure the navigation bar is hidden + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView() + } +} diff --git a/MobileAcebook/Models/Post.swift b/MobileAcebook/Models/Post.swift index 90f95b9e..0a213557 100644 --- a/MobileAcebook/Models/Post.swift +++ b/MobileAcebook/Models/Post.swift @@ -11,7 +11,17 @@ struct Post: Codable, Identifiable { let id: String let message: String let createdAt: String - let createdBy: User + let createdBy: User // The user data associated with the post let imgUrl: String? - let likes: [String] // List of user IDs who liked the post + let likes: [String] + + enum CodingKeys: String, CodingKey { + case id = "_id" // Map MongoDB _id to id in Swift + case message + case createdAt + case createdBy + case imgUrl + case likes + } } + diff --git a/MobileAcebook/Models/User.swift b/MobileAcebook/Models/User.swift index e75a5adf..4c3343c5 100644 --- a/MobileAcebook/Models/User.swift +++ b/MobileAcebook/Models/User.swift @@ -11,4 +11,11 @@ struct User: Codable, Identifiable { let email: String let username: String let imgUrl: String? + + enum CodingKeys: String, CodingKey { + case id = "_id" // Map the MongoDB _id to id in Swift + case email + case username + case imgUrl + } } diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index 5dae725d..a1222035 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -1,11 +1,18 @@ import UIKit import Foundation +// PostService to handle network requests related to posts class PostService { static let shared = PostService() private static let baseURL = "http://localhost:3000" private init() {} + + // Response struct to decode backend response that contains posts + struct PostResponse: Codable { + let posts: [Post] + let token: String? + } // Fetch all posts static func fetchPosts() async throws -> [Post] { @@ -23,10 +30,15 @@ class PostService { if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { - let posts = try JSONDecoder().decode([Post].self, from: data) - return posts + // Log the JSON data to debug + if let jsonString = String(data: data, encoding: .utf8) { + print("Response JSON: \(jsonString)") + } + + // Decode the response object that includes the posts array + let decodedResponse = try JSONDecoder().decode(PostResponse.self, from: data) + return decodedResponse.posts // Extract the array of posts from the response object } else { - // Handle non-200 responses let errorMessage = "Failed to fetch posts: HTTP \(httpResponse.statusCode)" print(errorMessage) throw NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]) diff --git a/MobileAcebook/SignUpView.swift b/MobileAcebook/SignUpView.swift index f2a63dab..bc6ebd5f 100644 --- a/MobileAcebook/SignUpView.swift +++ b/MobileAcebook/SignUpView.swift @@ -1,108 +1,133 @@ -import Foundation import SwiftUI -struct Constants { - static let ColorsBlue: Color = Color(red: 0, green: 0.48, blue: 1) - static let GraysWhite: Color = .white -} - struct SignUpView: View { - // State variables for user input and error handling - @State private var username: String = "" + @State private var username: String = "" // Adding username field @State private var email: String = "" @State private var password: String = "" @State private var errorMessage: String? + @State private var isSignUpSuccessful: Bool = false + @State private var isLoginViewPresented: Bool = false // State to trigger LoginView presentation - // Submit function for signing up - func submit() { - AuthenticationService.shared.signUp(username: username, email: email, password: password) { success, error in + // Function to handle sign-up + func submitSignUp() { + AuthenticationService.shared.signUp(username: username, email: email.lowercased(), password: password) { success, error in // Ensure email is passed in lowercase if success { - // Handle successful sign-up, such as navigating to a new view - print("User signed up successfully") + // Navigate to MainView after successful sign-up + DispatchQueue.main.async { + print("User signed up successfully") + isSignUpSuccessful = true // This triggers the NavigationLink + } } else { // Show error message - errorMessage = error + DispatchQueue.main.async { + errorMessage = error + } } } } var body: some View { - VStack { - Text("Sign Up!") - .font(.system(size: 40, weight: .bold, design: .default)) - .multilineTextAlignment(.center) - .foregroundColor(.black) - .frame(width: 288, height: 79, alignment: .center) - + NavigationStack { VStack { + Spacer() + + Text("Sign Up!") + .font(.system(size: 40, weight: .bold, design: .default)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + .frame(width: 288, height: 79, alignment: .center) + VStack { - // Username input - TextField("Enter Username", text: $username) - .padding(.leading, 16) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(Color.white.opacity(0.95)) - .font(Font.custom("SF Pro", size: 17)) - - // Email input - TextField("Enter Email", text: $email) - .padding(.leading, 16) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(Color.white.opacity(0.95)) - - // Password input - SecureField("Enter Password", text: $password) - .padding(.leading, 16) - .padding(.vertical, 15) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(Color.white.opacity(0.95)) - } - .padding(0) - .padding(.bottom) - .frame(width: 302, height: 242, alignment: .center) - .cornerRadius(10) - - // Show error message if any - if let errorMessage = errorMessage { - Text(errorMessage) - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding() - } - - // Sign Up button - HStack(alignment: .center, spacing: 3) { - Button(action: submit) { - Text("Sign Up!") - .font(Font.custom("SF Pro", size: 20)) - .foregroundColor(Constants.GraysWhite) + VStack { + // Username input field + TextField("Enter Username", text: $username) + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(Color.white.opacity(0.95)) + .font(Font.custom("SF Pro", size: 17)) + + // Email input field + TextField("Enter Email", text: $email) + .onChange(of: email) { newValue in + // Force the email text to lowercase + email = newValue.lowercased() + } + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(Color.white.opacity(0.95)) + .font(Font.custom("SF Pro", size: 17)) + + // Password input field + SecureField("Enter Password", text: $password) + .padding(.leading, 16) + .padding(.vertical, 15) + .frame(maxWidth: .infinity, alignment: .topLeading) + .background(Color.white.opacity(0.95)) + } + .padding(0) + .padding(.bottom) + .frame(width: 302, height: 240, alignment: .center) + .cornerRadius(10) + + // Show error message if any + if let errorMessage = errorMessage { + Text(errorMessage) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + } + + // Sign Up button + HStack(alignment: .center, spacing: 3) { + Button(action: submitSignUp) { + Text("Sign Up!") + .font(Font.custom("SF Pro", size: 20)) + .foregroundColor(.white) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: 113, height: 48, alignment: .center) + .background(Color.blue) + .cornerRadius(40) + + // Already have an account? Log in prompt + HStack(alignment: .center, spacing: 0) { + Button(action: { + isLoginViewPresented = true // Trigger LoginView presentation + }) { + Text("Already have an account? Log in") + .font(Font.custom("SF Pro", size: 18)) + .multilineTextAlignment(.center) + .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) + .frame(width: 272, height: 43, alignment: .top) + } + } + .padding(0) + .frame(width: 272, height: 43, alignment: .center) + + // NavigationLink to MainView, activated when signed up + NavigationLink(destination: MainView(), isActive: $isSignUpSuccessful) { + EmptyView() + } + + // NavigationLink back to LoginView + NavigationLink(destination: LoginView(), isActive: $isLoginViewPresented) { + EmptyView() } } - .padding(.horizontal, 10) - .padding(.vertical, 4) - .frame(width: 113, height: 48, alignment: .center) - .background(Constants.ColorsBlue) - .cornerRadius(40) - - // Already have an account? Login prompt - HStack(alignment: .center, spacing: 0) { - Text("Already have an account? Login") - .font(Font.custom("SF Pro", size: 18)) - .multilineTextAlignment(.center) - .foregroundColor(Color(red: 0, green: 0.48, blue: 1)) - .frame(width: 272, height: 43, alignment: .top) - } - .padding(0) - .frame(width: 272, height: 43, alignment: .center) + .frame(width: 335, height: 432) + .background(Color.white.opacity(0.75)) + .cornerRadius(48) + + Spacer() } - .frame(width: 335, height: 432) - .background(Color.white.opacity(0.75)) - .cornerRadius(48) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0, green: 0.96, blue: 1)) + .statusBar(hidden: false) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0, green: 0.96, blue: 1)) - .statusBar(hidden: false) } } diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/WelcomePageView.swift index e60ba913..d75d9329 100644 --- a/MobileAcebook/WelcomePageView.swift +++ b/MobileAcebook/WelcomePageView.swift @@ -1,34 +1,34 @@ -// -// WelcomePageView.swift -// MobileAcebook -// -// Created by Josué Estévez Fernández on 30/09/2023. -// - import SwiftUI struct WelcomePageView: View { + @State private var navigateToLogin = false + @State private var navigateToSignUp = false + var body: some View { - NavigationView { - VStack { - Spacer() - - Text("Acebook") - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.black) - - Spacer() - - Text("You are not logged in.\nPlease login or sign up") - .multilineTextAlignment(.center) - .padding() - .background(Color.white.opacity(0.8)) - .cornerRadius(10) - .padding(.horizontal) - - HStack { - NavigationLink(destination: SignUpView()) { + NavigationStack { + VStack { + Spacer() + + Text("Acebook") + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.black) + + Spacer() + + Text("You are not logged in.\nPlease login or sign up") + .multilineTextAlignment(.center) + .padding() + .background(Color.white.opacity(0.8)) + .cornerRadius(10) + .padding(.horizontal) + + HStack { + // Sign Up Button with NavigationLink + NavigationLink(destination: SignUpView(), isActive: $navigateToSignUp) { + Button(action: { + navigateToSignUp = true + }) { Text("Sign Up") .foregroundColor(.blue) .padding() @@ -37,8 +37,13 @@ struct WelcomePageView: View { .cornerRadius(10) .padding(.horizontal, 5) } - - NavigationLink(destination: LoginView()) { + } + + // Login Button with NavigationLink + NavigationLink(destination: LoginView(), isActive: $navigateToLogin) { + Button(action: { + navigateToLogin = true + }) { Text("Login") .foregroundColor(.blue) .padding() @@ -48,14 +53,15 @@ struct WelcomePageView: View { .padding(.horizontal, 5) } } - .padding() - - Spacer() } - .background(Color.cyan) - .edgesIgnoringSafeArea(.all) + .padding() + + Spacer() } + .background(Color.cyan) + .navigationBarHidden(true) // Hide navigation bar for welcome screen } + } } struct WelcomePageView_Previews: PreviewProvider { From 0dd3bb8babffced26c02764312524ed5f5783f67 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 5 Sep 2024 10:56:18 +0100 Subject: [PATCH 23/29] Fixed fetch posts --- MobileAcebook/Services/PostService.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index a1222035..0fdf4c0a 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -22,6 +22,7 @@ class PostService { var request = URLRequest(url: url) if let token = AuthenticationService.shared.getToken() { + print(token) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } From e281ab9f96df59655b5222dbc0864c1ff8090274 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:01:58 +0100 Subject: [PATCH 24/29] Added photo upload and displaying on posts --- MobileAcebook.xcodeproj/project.pbxproj | 4 ++ MobileAcebook/CreatePostView.swift | 32 ++++++++++++++-- MobileAcebook/FeedView.swift | 45 ++++++++++++++++------ MobileAcebook/PhotoPicker.swift | 48 ++++++++++++++++++++++++ MobileAcebook/Services/PostService.swift | 48 +++++++++++++----------- 5 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 MobileAcebook/PhotoPicker.swift diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 351742ae..121a5d2c 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ AE5D85E32AC9AFD2009680C6 /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */; }; AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */; }; AE5D85E82AC9B29A009680C6 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5D85E72AC9B29A009680C6 /* User.swift */; }; + F82DA57C2C89BC6800CA8A56 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82DA57B2C89BC6800CA8A56 /* PhotoPicker.swift */; }; F8304C5D2C888BF000B4BBC9 /* FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8304C5C2C888BF000B4BBC9 /* FeedView.swift */; }; F8304C5F2C888C0500B4BBC9 /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8304C5E2C888C0500B4BBC9 /* PostCardView.swift */; }; F83545452C875D9300AB9C9E /* FullPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83545442C875D9300AB9C9E /* FullPostViewModel.swift */; }; @@ -71,6 +72,7 @@ AE5D85E22AC9AFD2009680C6 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; AE5D85E52AC9B077009680C6 /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; AE5D85E72AC9B29A009680C6 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + F82DA57B2C89BC6800CA8A56 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; F8304C5C2C888BF000B4BBC9 /* FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedView.swift; sourceTree = ""; }; F8304C5E2C888C0500B4BBC9 /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = ""; }; F83545442C875D9300AB9C9E /* FullPostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullPostViewModel.swift; sourceTree = ""; }; @@ -150,6 +152,7 @@ F8304C5C2C888BF000B4BBC9 /* FeedView.swift */, F8304C5E2C888C0500B4BBC9 /* PostCardView.swift */, F87BD8262C88AF5E0071F4D3 /* MainView.swift */, + F82DA57B2C89BC6800CA8A56 /* PhotoPicker.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -363,6 +366,7 @@ F8304C5D2C888BF000B4BBC9 /* FeedView.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, F87BD8272C88AF5E0071F4D3 /* MainView.swift in Sources */, + F82DA57C2C89BC6800CA8A56 /* PhotoPicker.swift in Sources */, F83545452C875D9300AB9C9E /* FullPostViewModel.swift in Sources */, F844A8B22C87480F007EA48A /* Comment.swift in Sources */, F844A8AE2C874802007EA48A /* UserService.swift in Sources */, diff --git a/MobileAcebook/CreatePostView.swift b/MobileAcebook/CreatePostView.swift index 3b980fff..a7d76965 100644 --- a/MobileAcebook/CreatePostView.swift +++ b/MobileAcebook/CreatePostView.swift @@ -1,10 +1,14 @@ import SwiftUI +import PhotosUI struct CreatePostView: View { @State private var userInput: String = "" @State private var showAlert: Bool = false @State private var alertTitle: String = "" @State private var alertMessage: String = "" + @State private var selectedImage: UIImage? = nil + @State private var showPhotoPicker = false + @State private var isUploadingImage = false @Environment(\.presentationMode) var presentationMode // Handle modal dismissal var body: some View { @@ -41,10 +45,19 @@ struct CreatePostView: View { .frame(minWidth: 100, maxWidth: 400, minHeight: 100, maxHeight: 250) .padding(.horizontal, 20) + // Show selected image preview + if let image = selectedImage { + Image(uiImage: image) + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .cornerRadius(10) + } + // Action Buttons - Centered HStack(alignment: .center, spacing: 20) { Button("Add Image") { - // Add Image action if necessary + showPhotoPicker = true // Show the photo picker } .frame(width: 120, height: 44) .background(Color.blue) @@ -52,11 +65,17 @@ struct CreatePostView: View { .foregroundColor(.white) Button("Create Post") { - // Create Post action Task { do { - // The token is automatically handled in PostService, so no need to pass it manually - _ = try await PostService.createPost(message: userInput, image: nil) + var imageUrl: String? = nil + if let selectedImage = selectedImage { + isUploadingImage = true + // Upload the image to Cloudinary and get the image URL + imageUrl = try await PostService.uploadImageToCloudinary(image: selectedImage) + } + + // Create the post with or without an image URL + _ = try await PostService.createPost(message: userInput, image: selectedImage) // Show success alert alertTitle = "Post Created" @@ -75,6 +94,7 @@ struct CreatePostView: View { .background(Color.blue) .cornerRadius(40) .foregroundColor(.white) + .disabled(isUploadingImage) // Disable if image is uploading } .padding(.top, 30) @@ -96,6 +116,10 @@ struct CreatePostView: View { } .background(Color(red: 0, green: 0.96, blue: 1).ignoresSafeArea()) // Cover entire screen with background color .navigationBarHidden(true) // Hide default navigation bar + .sheet(isPresented: $showPhotoPicker) { + // Use SwiftUI's photo picker + PhotoPicker(selectedImage: $selectedImage) + } } } diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift index 33a51bf0..e402bdb2 100644 --- a/MobileAcebook/FeedView.swift +++ b/MobileAcebook/FeedView.swift @@ -18,12 +18,15 @@ struct FeedView: View { .padding() } else { ScrollView { - // Display the posts using PostView - ForEach(posts) { post in - PostView(post: post) - .padding(.bottom, 10) + VStack(spacing: 10) { // Add some spacing between posts + // Display the posts in reversed order (newest first) + ForEach(posts.reversed()) { post in + PostView(post: post) + .padding(.bottom, 10) + } } .padding(.horizontal) + .padding(.bottom, 100) // Extra padding to avoid overlap with the navigation bar } } } @@ -52,30 +55,48 @@ struct FeedView: View { } } } - } #Preview { FeedView() } - struct PostView: View { let post: Post var body: some View { ZStack { - Rectangle() - .foregroundColor(.clear) - .frame(width: 192, height: 217) - .background(Color(red: 0.85, green: 0.85, blue: 0.85)) - .cornerRadius(48) - .padding(.trailing, 140) + // The grey background placeholder or image + if let imgUrl = post.imgUrl, let url = URL(string: imgUrl) { + AsyncImage(url: url) { image in + image + .resizable() + .frame(width: 192, height: 217) // Same size as before + .cornerRadius(48) + .padding(.trailing, 140) // Image aligned to left with padding + } placeholder: { + Rectangle() + .foregroundColor(Color.gray.opacity(0.3)) + .frame(width: 192, height: 217) + .cornerRadius(48) + .padding(.trailing, 140) + } + } else { + Rectangle() + .foregroundColor(Color.gray.opacity(0.3)) + .frame(width: 192, height: 217) + .cornerRadius(48) + .padding(.trailing, 140) + } + + // Post message on the right side Text("\(post.message)") .font(Font.custom("SF Pro", size: 17)) .foregroundColor(.black) .frame(width: 135, height: 137, alignment: .topLeading) .padding(.leading, 200) + + // Heart icon to show like status Image(systemName: checkIfLiked(userId: post.id, post: post) ? "heart.fill" : "heart") .resizable() .frame(width: 35, height: 35) diff --git a/MobileAcebook/PhotoPicker.swift b/MobileAcebook/PhotoPicker.swift new file mode 100644 index 00000000..4428b8bb --- /dev/null +++ b/MobileAcebook/PhotoPicker.swift @@ -0,0 +1,48 @@ +// +// PhotoPicker.swift +// MobileAcebook +// +// Created by Sam Quincey on 05/09/2024. +// + +import SwiftUI +import PhotosUI + +struct PhotoPicker: UIViewControllerRepresentable { + @Binding var selectedImage: UIImage? + + class Coordinator: NSObject, PHPickerViewControllerDelegate { + var parent: PhotoPicker + + init(parent: PhotoPicker) { + self.parent = parent + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + + if let result = results.first { + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in + guard let self = self, let uiImage = image as? UIImage, error == nil else { return } + DispatchQueue.main.async { + self.parent.selectedImage = uiImage + } + } + } + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> PHPickerViewController { + var config = PHPickerConfiguration() + config.filter = .images + let picker = PHPickerViewController(configuration: config) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} +} diff --git a/MobileAcebook/Services/PostService.swift b/MobileAcebook/Services/PostService.swift index 0fdf4c0a..21769663 100644 --- a/MobileAcebook/Services/PostService.swift +++ b/MobileAcebook/Services/PostService.swift @@ -5,6 +5,8 @@ import Foundation class PostService { static let shared = PostService() private static let baseURL = "http://localhost:3000" + private static let CLOUDINARY_CLOUD_NAME = "dq51orqba" + private static let CLOUDINARY_UPLOAD_PRESET = "jr6ol490" private init() {} @@ -22,7 +24,7 @@ class PostService { var request = URLRequest(url: url) if let token = AuthenticationService.shared.getToken() { - print(token) + print("Token: \(token)") // Debug: Token output request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } @@ -31,14 +33,12 @@ class PostService { if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { - // Log the JSON data to debug if let jsonString = String(data: data, encoding: .utf8) { - print("Response JSON: \(jsonString)") + print("Response JSON: \(jsonString)") // Debug: Response JSON output } - // Decode the response object that includes the posts array let decodedResponse = try JSONDecoder().decode(PostResponse.self, from: data) - return decodedResponse.posts // Extract the array of posts from the response object + return decodedResponse.posts } else { let errorMessage = "Failed to fetch posts: HTTP \(httpResponse.statusCode)" print(errorMessage) @@ -48,7 +48,7 @@ class PostService { throw NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"]) } } catch { - print("Error fetching posts: \(error)") + print("Error fetching posts: \(error)") // Debug: Error fetching posts throw error } } @@ -56,12 +56,12 @@ class PostService { // Create a new post with optional image static func createPost(message: String, image: UIImage?) async throws -> Bool { if let image = image { - // If the user selected an image, upload it to Cloudinary first + print("Image selected for upload.") // Debug: Image selected let url = try await uploadImageToCloudinary(image: image) - // After getting the image URL, create the post with the image + print("Image uploaded to Cloudinary: \(url)") // Debug: Cloudinary image URL return try await createPostWithImage(message: message, imgUrl: url) } else { - // If no image was selected, create the post without an image + print("No image selected for upload.") // Debug: No image selected return try await createPostWithImage(message: message, imgUrl: nil) } } @@ -101,11 +101,10 @@ class PostService { } // Upload image to Cloudinary - static private func uploadImageToCloudinary(image: UIImage) async throws -> String { - guard let cloudName = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_CLOUD_NAME") as? String, - let uploadPreset = Bundle.main.object(forInfoDictionaryKey: "CLOUDINARY_UPLOAD_PRESET") as? String else { - throw NSError(domain: "CloudinaryError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cloudinary credentials not found."]) - } + static internal func uploadImageToCloudinary(image: UIImage) async throws -> String { + // Directly use the constants since they are not optional + let cloudName = CLOUDINARY_CLOUD_NAME + let uploadPreset = CLOUDINARY_UPLOAD_PRESET let url = URL(string: "https://api.cloudinary.com/v1_1/\(cloudName)/image/upload")! @@ -122,24 +121,31 @@ class PostService { data.append("\(uploadPreset)\r\n".data(using: .utf8)!) if let imageData = image.jpegData(compressionQuality: 0.7) { + print("Image data size: \(imageData.count) bytes") // Debug: Image data size data.append("--\(boundary)\r\n".data(using: .utf8)!) data.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!) data.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) data.append(imageData) data.append("\r\n".data(using: .utf8)!) + } else { + throw NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert UIImage to JPEG data"]) } data.append("--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = data - let (responseData, _) = try await URLSession.shared.data(for: request) - - if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any], - let url = json["secure_url"] as? String { - return url - } else { - throw NSError(domain: "CloudinaryError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to upload image."]) + do { + let (responseData, _) = try await URLSession.shared.data(for: request) + if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any], + let url = json["secure_url"] as? String { + return url + } else { + throw NSError(domain: "CloudinaryError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to upload image."]) + } + } catch { + print("Error uploading image to Cloudinary: \(error)") // Debug: Error uploading image + throw error } } From 6ff911f663b8a519df64d9c2613b87e395c45995 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:39:18 +0100 Subject: [PATCH 25/29] Fixed full post view and comments --- MobileAcebook/FeedView.swift | 30 +++++-- MobileAcebook/FullPostView.swift | 88 +++++++++++++++------ MobileAcebook/MainView.swift | 14 ++-- MobileAcebook/Models/Comment.swift | 27 ++++++- MobileAcebook/Services/CommentService.swift | 34 ++++++-- 5 files changed, 147 insertions(+), 46 deletions(-) diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift index e402bdb2..6b319a78 100644 --- a/MobileAcebook/FeedView.swift +++ b/MobileAcebook/FeedView.swift @@ -1,6 +1,7 @@ import SwiftUI struct FeedView: View { + @Binding var shouldRefresh: Bool // Use binding to trigger refresh @State private var posts: [Post] = [] // To store the fetched posts @State private var isLoading: Bool = true // To show loading state @State private var errorMessage: String? // To handle and show errors @@ -33,6 +34,12 @@ struct FeedView: View { .onAppear { fetchPosts() // Fetch posts when the view appears } + .onChange(of: shouldRefresh) { newValue in + if newValue { + fetchPosts() // Refetch posts when shouldRefresh is true + shouldRefresh = false // Reset the refresh flag + } + } .background(Color(red: 0, green: 0.48, blue: 1).opacity(0.28)) .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -57,13 +64,10 @@ struct FeedView: View { } } -#Preview { - FeedView() -} - struct PostView: View { let post: Post - + @State private var showFullPostView = false // State to control showing FullPostView + var body: some View { ZStack { // The grey background placeholder or image @@ -74,12 +78,18 @@ struct PostView: View { .frame(width: 192, height: 217) // Same size as before .cornerRadius(48) .padding(.trailing, 140) // Image aligned to left with padding + .onTapGesture { + showFullPostView.toggle() // Show FullPostView when tapped + } } placeholder: { Rectangle() .foregroundColor(Color.gray.opacity(0.3)) .frame(width: 192, height: 217) .cornerRadius(48) .padding(.trailing, 140) + .onTapGesture { + showFullPostView.toggle() // Show FullPostView even if no image is present + } } } else { Rectangle() @@ -87,6 +97,9 @@ struct PostView: View { .frame(width: 192, height: 217) .cornerRadius(48) .padding(.trailing, 140) + .onTapGesture { + showFullPostView.toggle() // Show FullPostView if no image is present + } } // Post message on the right side @@ -107,9 +120,14 @@ struct PostView: View { .frame(width: 393, height: 259) .background(.white) .cornerRadius(48) + .fullScreenCover(isPresented: $showFullPostView) { + // Show FullPostView in full screen when triggered + FullPostView(postId: post.id, token: AuthenticationService.shared.getToken() ?? "") + } } } + // Helper function to check if a post is liked func checkIfLiked(userId: String, post: Post) -> Bool { return post.likes.contains(userId) @@ -117,6 +135,6 @@ func checkIfLiked(userId: String, post: Post) -> Bool { struct FeedView_Previews: PreviewProvider { static var previews: some View { - FeedView() + FeedView(shouldRefresh: .constant(false)) } } diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift index e56d9981..fe861822 100644 --- a/MobileAcebook/FullPostView.swift +++ b/MobileAcebook/FullPostView.swift @@ -1,9 +1,3 @@ -// -// FullPostView.swift -// MobileAcebook -// -// Created by Sam Quincey on 03/09/2024. -// import SwiftUI struct FullPostView: View { @@ -11,12 +5,33 @@ struct FullPostView: View { let postId: String let token: String + @State private var commentText: String = "" // To store the new comment text + @State private var isAddingComment = false // To track comment submission + @State private var submissionError: Bool = false // Handle errors during comment submission + + @Environment(\.dismiss) private var dismiss // For dismissing the view + var body: some View { VStack { + // Dismiss button + HStack { + Button(action: { + dismiss() // Close the view when tapped + }) { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 30, height: 30) + .foregroundColor(.black) + .padding(.leading, 20) + .padding(.top, 10) + } + Spacer() + } + ScrollView { VStack(alignment: .leading, spacing: 16) { if viewModel.hasError { - mockPostView + mockPostView // If there's an error, show the mock post } else if let post = viewModel.post { // Display the image and message... if let imageUrl = post.imgUrl { @@ -76,23 +91,54 @@ struct FullPostView: View { } } - // Add Comment Button - HStack { - Spacer() - Button(action: { - // Handle adding a comment (e.g., show a sheet or navigate to a new view) - }) { - Image(systemName: "plus.circle.fill") - .resizable() - .frame(width: 44, height: 44) - .foregroundColor(.blue) + // Add Comment Section + VStack { + HStack { + TextField("Add a comment...", text: $commentText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .frame(height: 44) + .padding(.horizontal) + + Button(action: { + if !commentText.isEmpty { + isAddingComment = true + submissionError = false // Reset any previous submission error + CommentService.shared.createComment(message: commentText, forPostId: postId) { success, error in + if success { + // Comment added successfully, now refresh the comments + viewModel.fetchComments(postId: postId, token: token) + commentText = "" // Clear the text field + } else { + // Handle error during comment submission + submissionError = true + } + isAddingComment = false + } + } + }) { + Image(systemName: "paperplane.fill") + .resizable() + .frame(width: 44, height: 44) + .foregroundColor(.blue) + } + .disabled(isAddingComment) // Disable button when adding comment + .padding(.trailing) + } + .padding(.horizontal) + + // Show error message if comment submission fails + if submissionError { + Text("Failed to submit comment. Please try again.") + .foregroundColor(.red) + .font(.caption) + .padding(.horizontal) } - Spacer().frame(width: 20) } .padding() } .navigationBarTitleDisplayMode(.inline) .onAppear { + // Fetch the post and comments when the view appears viewModel.fetchPost(postId: postId, token: token) viewModel.fetchComments(postId: postId, token: token) } @@ -176,10 +222,4 @@ struct FullPostView: View { .padding(.horizontal) } } - struct FullPostView_Previews: PreviewProvider { - static var previews: some View { - FullPostView(postId: "examplePostId", token: "exampleToken") - } - } - diff --git a/MobileAcebook/MainView.swift b/MobileAcebook/MainView.swift index f50cc76a..2f5e6184 100644 --- a/MobileAcebook/MainView.swift +++ b/MobileAcebook/MainView.swift @@ -4,6 +4,7 @@ struct MainView: View { @State private var isLogoutPopupShowing = false // Control logout pop-up visibility @State private var showCreatePostView = false // Control showing the Create Post view @State private var navigateToWelcome = false // Handle navigation to WelcomePageView after logout + @State private var shouldRefreshFeed = false // Trigger feed refresh init() { // Configure tab bar appearance @@ -18,8 +19,8 @@ struct MainView: View { var body: some View { ZStack { - // Show Feed by default - FeedView() + // Show Feed and pass in the refresh control + FeedView(shouldRefresh: $shouldRefreshFeed) VStack { Spacer() // Pushes the tab bar to the bottom @@ -50,9 +51,10 @@ struct MainView: View { } Spacer() - // Refresh Button (Placeholder action) + // Refresh Button (Triggers feed refresh) Button(action: { print("Refreshing feed...") + shouldRefreshFeed = true // Set refresh flag to true }) { VStack { Image(systemName: "arrow.clockwise") @@ -88,9 +90,3 @@ struct MainView: View { .navigationBarHidden(true) } } - -struct MainView_Previews: PreviewProvider { - static var previews: some View { - MainView() - } -} diff --git a/MobileAcebook/Models/Comment.swift b/MobileAcebook/Models/Comment.swift index c914a91c..d5c753c7 100644 --- a/MobileAcebook/Models/Comment.swift +++ b/MobileAcebook/Models/Comment.swift @@ -10,6 +10,31 @@ import Foundation struct Comment: Codable, Identifiable { let id: String let message: String // The text content of the comment - let createdAt: Date // The creation date of the comment + let createdAt: String // The creation date of the comment let createdBy: User // The user who created the comment + + enum CodingKeys: String, CodingKey { + case id = "_id" // Map MongoDB _id to id in Swift + case message + case createdAt + case createdBy + } + +// // Custom initializer to decode the `createdAt` field as a Date from a String +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// +// id = try container.decode(String.self, forKey: .id) +// message = try container.decode(String.self, forKey: .message) +// createdBy = try container.decode(User.self, forKey: .createdBy) +// +// // Decode `createdAt` as a string and convert it to a `Date` +// let createdAtString = try container.decode(String.self, forKey: .createdAt) +// let formatter = ISO8601DateFormatter() +// if let date = formatter.date(from: createdAtString) { +// createdAt = date +// } else { +// throw DecodingError.dataCorruptedError(forKey: .createdAt, in: container, debugDescription: "Date string does not match format expected by formatter.") +// } +// } } diff --git a/MobileAcebook/Services/CommentService.swift b/MobileAcebook/Services/CommentService.swift index 0bed792e..2b5ac9a2 100644 --- a/MobileAcebook/Services/CommentService.swift +++ b/MobileAcebook/Services/CommentService.swift @@ -9,34 +9,56 @@ class CommentService { // Fetch comments for a specific post func fetchComments(forPostId postId: String, completion: @escaping ([Comment]?, Error?) -> Void) { guard let url = URL(string: "\(baseURL)/comments/\(postId)") else { return } - + var request = URLRequest(url: url) request.httpMethod = "GET" + + // Add token if available if let token = AuthenticationService.shared.getToken() { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } + // Define the CommentResponse structure within the function + struct CommentResponse: Codable { + let message: String + let comments: [Comment] + let token: String + } + let task = URLSession.shared.dataTask(with: request) { data, response, error in + // Handle network error if let error = error { completion(nil, error) return } - + + // Handle HTTP error + if let httpResponse = response as? HTTPURLResponse { + if httpResponse.statusCode != 200 { + let statusError = NSError(domain: "HTTPError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Server returned status code \(httpResponse.statusCode)"]) + completion(nil, statusError) + return + } + } + + // Ensure there's valid data guard let data = data else { completion(nil, NSError(domain: "DataError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data returned"])) return } - + + // Try decoding the response do { - let comments = try JSONDecoder().decode([Comment].self, from: data) - completion(comments, nil) + let commentResponse = try JSONDecoder().decode(CommentResponse.self, from: data) + completion(commentResponse.comments, nil) // Pass comments array to the completion handler } catch let jsonError { completion(nil, jsonError) } } - + task.resume() } + // Create a new comment for a specific post func createComment(message: String, forPostId postId: String, completion: @escaping (Bool, Error?) -> Void) { From 03b19ab380a8b2b6f31d0b13e4a37a136983a8dd Mon Sep 17 00:00:00 2001 From: mqzcn <57679249+mqzcn@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:25:58 +0100 Subject: [PATCH 26/29] Update README.md --- README.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8fc42aff..ff77dd8b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ -# SwiftUI Project - -As part of this project, you will create a new SwiftUI app that integrates with -[this Acebook backend](https://github.com/makersacademy/acebook-mobile-express-mongo-backend/tree/main). +# Mobile Acebook SwiftUI +Front-end application for Mobile Acebook project - by: +Karina, Maz, Robert, Sam and Will ## This Repo -This repo contains the seed project for your SwiftUI front-end application. +This repo contains the project for a Swift-UI frontend application consuming an Express backend that lives [here](https://github.com/QS-Coding/Mobile-Acebook) + +## Installation + 1. Clone the front-end and back-end repos to your local machine + 2. Navigate to /api in the backend and run `npm install` + 3. Create .env file in /api and add `MONGODB_URL` and `JWT_SECRET` environment variables + 4. Start backend server with command `npm start` + 5. Open frontend folder with XCode + 6. Build Swift app using XCode's top menu -> Product -> Build + 7. Run Swift app using XCode's top menu -> Product -> Run + 8. Simulator window will appear with app running + +## Usage +1. App allows user to Signup / Login +2. User navigated to Feed upon successful Login +3. User can create new posts (with an optional image) by clicking "Create Post" on navbar +4. User navigated to Feed upon successful Post creation +5. User can refresh Feed by clicking "Refresh" on navbar +6. User can logout by clicking "Logout" on navbar and confirming on alert/popup. From 24748428b7d9ebb158d229d3639c2afe85e8d976 Mon Sep 17 00:00:00 2001 From: mqzcn <57679249+mqzcn@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:28:12 +0100 Subject: [PATCH 27/29] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff77dd8b..7d80e88e 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,7 @@ This repo contains the project for a Swift-UI frontend application consuming an 2. User navigated to Feed upon successful Login 3. User can create new posts (with an optional image) by clicking "Create Post" on navbar 4. User navigated to Feed upon successful Post creation -5. User can refresh Feed by clicking "Refresh" on navbar -6. User can logout by clicking "Logout" on navbar and confirming on alert/popup. +5. User can click on a post in Feed and is navigated to Full Post View. +6. User can read and add comments to post in Full Post View. +7. User can refresh Feed by clicking "Refresh" on navbar +8. User can logout by clicking "Logout" on navbar and confirming on alert/popup. From 8f3cfbef694a77349f8a361eaf7685e430093e60 Mon Sep 17 00:00:00 2001 From: mqzcn <57679249+mqzcn@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:29:19 +0100 Subject: [PATCH 28/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d80e88e..3a130e6e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Karina, Maz, Robert, Sam and Will This repo contains the project for a Swift-UI frontend application consuming an Express backend that lives [here](https://github.com/QS-Coding/Mobile-Acebook) ## Installation - 1. Clone the front-end and back-end repos to your local machine + 1. Clone the front-end and [back-end](https://github.com/QS-Coding/Mobile-Acebook) repos to your local machine 2. Navigate to /api in the backend and run `npm install` 3. Create .env file in /api and add `MONGODB_URL` and `JWT_SECRET` environment variables 4. Start backend server with command `npm start` From 04f0051d776792202c7c78f67e0f8a0dc43587fd Mon Sep 17 00:00:00 2001 From: mqzcn <57679249+mqzcn@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:49:19 +0100 Subject: [PATCH 29/29] Update README.md --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a130e6e..f459ff99 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,25 @@ This repo contains the project for a Swift-UI frontend application consuming an ## Installation 1. Clone the front-end and [back-end](https://github.com/QS-Coding/Mobile-Acebook) repos to your local machine 2. Navigate to /api in the backend and run `npm install` - 3. Create .env file in /api and add `MONGODB_URL` and `JWT_SECRET` environment variables - 4. Start backend server with command `npm start` - 5. Open frontend folder with XCode - 6. Build Swift app using XCode's top menu -> Product -> Build - 7. Run Swift app using XCode's top menu -> Product -> Run - 8. Simulator window will appear with app running + 3. Install MongoDB (ONLY IF YOU HAVEN'T ALREADY - skip this step if so, to avoid running into issues) + ``` + brew tap mongodb/brew + brew install mongodb-community@6.0 + ``` + *Note:* If you see a message that says `If you need to have + mongodb-community@5.0 first in your PATH, run:`, follow the instruction. + Restart your terminal after this. + + 4. Start MongoDB + ``` + brew services start mongodb-community@6.0 + ``` + 5. Create .env file in /api and add `MONGODB_URL` and `JWT_SECRET` environment variables + 6. Start backend server with command `npm run dev` + 7. Open frontend folder with XCode + 8. Build Swift app using XCode's top menu -> Product -> Build + 9. Run Swift app using XCode's top menu -> Product -> Run + 10. Simulator window will appear with app running ## Usage 1. App allows user to Signup / Login