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 afc3fdc36d500e1616258f280aade7dc4a8c3e54 Mon Sep 17 00:00:00 2001 From: QS-Coding <169077428+QS-Coding@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:12:19 +0100 Subject: [PATCH 26/29] Some fixes to likes --- MobileAcebook/FeedView.swift | 54 +++++++++++++++++++++++++++++--- MobileAcebook/FullPostView.swift | 4 +-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift index 6b319a78..3a9abc5b 100644 --- a/MobileAcebook/FeedView.swift +++ b/MobileAcebook/FeedView.swift @@ -67,6 +67,9 @@ struct FeedView: View { struct PostView: View { let post: Post @State private var showFullPostView = false // State to control showing FullPostView + @State private var isLiked: Bool = false // Track the current like status + @State private var likesCount: Int = 0 // Track the number of likes + @State private var currentUserId: String = AuthenticationService.shared.getUserId() ?? "" var body: some View { ZStack { @@ -110,29 +113,72 @@ struct PostView: View { .padding(.leading, 200) // Heart icon to show like status - Image(systemName: checkIfLiked(userId: post.id, post: post) ? "heart.fill" : "heart") + Image(systemName: isLiked ? "heart.fill" : "heart") .resizable() .frame(width: 35, height: 35) - .foregroundColor(checkIfLiked(userId: post.id, post: post) ? .red : .black) + .foregroundColor(isLiked ? .red : .black) .padding(.top, 200) .padding(.leading, 200) + .onTapGesture { + toggleLike() + } } .frame(width: 393, height: 259) .background(.white) .cornerRadius(48) + .onAppear { + // Set the initial like status and likes count + isLiked = checkIfLiked(userId: currentUserId, post: post) + likesCount = post.likes.count + } .fullScreenCover(isPresented: $showFullPostView) { - // Show FullPostView in full screen when triggered FullPostView(postId: post.id, token: AuthenticationService.shared.getToken() ?? "") } } -} + + // Function to toggle the like state and call the backend + private func toggleLike() { + Task { + // Optimistically toggle the like state + isLiked.toggle() + if isLiked { + likesCount += 1 + } else { + likesCount -= 1 + } + do { + let success = try await PostService.updateLikes(postId: post.id) + if !success { + // Revert the like state if the backend update fails + isLiked.toggle() + if isLiked { + likesCount += 1 + } else { + likesCount -= 1 + } + } + } catch { + // Handle error and revert like state if needed + print("Error updating likes: \(error.localizedDescription)") + isLiked.toggle() + if isLiked { + likesCount += 1 + } else { + likesCount -= 1 + } + } + } + } +} // 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(shouldRefresh: .constant(false)) diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift index fe861822..4fe35c0f 100644 --- a/MobileAcebook/FullPostView.swift +++ b/MobileAcebook/FullPostView.swift @@ -10,7 +10,7 @@ struct FullPostView: View { @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 @@ -221,5 +221,5 @@ struct FullPostView: View { } .padding(.horizontal) } -} +} From 5e33377b3d1b080d31e27df5acb02183c01054e0 Mon Sep 17 00:00:00 2001 From: Karina Dawson Date: Thu, 5 Sep 2024 16:07:34 +0100 Subject: [PATCH 27/29] welcomepage background and buttons changed and createpost box padding --- MobileAcebook/CreatePostView.swift | 32 +++++++++++++++++++---------- MobileAcebook/WelcomePageView.swift | 22 ++++++++------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/MobileAcebook/CreatePostView.swift b/MobileAcebook/CreatePostView.swift index a7d76965..9460a7a8 100644 --- a/MobileAcebook/CreatePostView.swift +++ b/MobileAcebook/CreatePostView.swift @@ -33,17 +33,27 @@ struct CreatePostView: View { .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) - .padding(.horizontal, 20) + ZStack { + // White background box + RoundedRectangle(cornerRadius: 30) + .fill(Color.white) + .frame(minWidth: 100, maxWidth: 300, minHeight: 120, maxHeight: 100) + .shadow(radius: 5) // Optional: Adds a shadow for better visibility + + // Post Text Field + TextField( + "Write what you would like to post...", + text: $userInput, + axis: .vertical + ) + .padding(.bottom, 120) //this is a hack to raise the text to the top + .padding(.horizontal, 16) + .frame(minWidth: 100, maxWidth: 300, minHeight: 100, maxHeight: 200) + .background(Color(.white)) + .cornerRadius(30) + .padding(.horizontal, 20) + } + // Show selected image preview if let image = selectedImage { diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/WelcomePageView.swift index d75d9329..69e9d7ff 100644 --- a/MobileAcebook/WelcomePageView.swift +++ b/MobileAcebook/WelcomePageView.swift @@ -30,12 +30,10 @@ struct WelcomePageView: View { navigateToSignUp = true }) { Text("Sign Up") - .foregroundColor(.blue) - .padding() - .frame(maxWidth: .infinity) - .background(Color.white.opacity(0.8)) - .cornerRadius(10) - .padding(.horizontal, 5) + .frame(width: 177, height: 54) + .background(Color.blue) + .cornerRadius(40) + .foregroundColor(.white) } } @@ -45,12 +43,10 @@ struct WelcomePageView: View { navigateToLogin = true }) { Text("Login") - .foregroundColor(.blue) - .padding() - .frame(maxWidth: .infinity) - .background(Color.white.opacity(0.8)) - .cornerRadius(10) - .padding(.horizontal, 5) + .frame(width: 177, height: 54) + .background(Color.blue) + .cornerRadius(40) + .foregroundColor(.white) } } } @@ -58,7 +54,7 @@ struct WelcomePageView: View { Spacer() } - .background(Color.cyan) + .background(Color(red: 0, green: 0.96, blue: 1).ignoresSafeArea()) .navigationBarHidden(true) // Hide navigation bar for welcome screen } } From 0705d052685782b619deaca12e58bed2779ac4aa Mon Sep 17 00:00:00 2001 From: Karina Dawson Date: Fri, 6 Sep 2024 12:38:21 +0100 Subject: [PATCH 28/29] styling changes to post and forms --- .../ace.imageset/Contents.json | 20 +++++++ MobileAcebook/FeedView.swift | 53 +++++++++++++------ MobileAcebook/FullPostView.swift | 3 +- MobileAcebook/LoginView.swift | 2 + MobileAcebook/PostCardView.swift | 27 ++++++---- MobileAcebook/SignUpView.swift | 5 +- MobileAcebook/WelcomePageView.swift | 4 +- 7 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 MobileAcebook/Assets.xcassets/ace.imageset/Contents.json diff --git a/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json b/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json new file mode 100644 index 00000000..a19a5492 --- /dev/null +++ b/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MobileAcebook/FeedView.swift b/MobileAcebook/FeedView.swift index 3a9abc5b..e8d40065 100644 --- a/MobileAcebook/FeedView.swift +++ b/MobileAcebook/FeedView.swift @@ -40,7 +40,7 @@ struct FeedView: View { shouldRefresh = false // Reset the refresh flag } } - .background(Color(red: 0, green: 0.48, blue: 1).opacity(0.28)) + .background(Color(red: 0, green: 0.96, blue: 1).ignoresSafeArea()) .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -105,23 +105,42 @@ struct PostView: View { } } - // 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) + + VStack(alignment: .leading) { + Text("Posted by: \(post.createdBy.username)") +// .font(Font.custom("SF Pro Bold", size: 17)) + .fontWeight(.bold) + .foregroundColor(.black) + .padding(.leading, 220) + + + Text("\(post.message)") + .font(Font.custom("SF Pro", size: 17)) + .foregroundColor(.black) + .frame(width: 135, height: 137, alignment: .topLeading) + .padding(.leading, 220) + } - // Heart icon to show like status - Image(systemName: isLiked ? "heart.fill" : "heart") - .resizable() - .frame(width: 35, height: 35) - .foregroundColor(isLiked ? .red : .black) - .padding(.top, 200) - .padding(.leading, 200) - .onTapGesture { - toggleLike() - } + + HStack(spacing: 20) { + Image(systemName: "message") + .resizable() + .frame(width: 38, height: 38) + .opacity(1.8) + .onTapGesture { + showFullPostView.toggle() + } + + Image(systemName: isLiked ? "heart.fill" : "heart") + .resizable() + .frame(width: 35, height: 35) + .foregroundColor(isLiked ? .red : .black) + .onTapGesture { + toggleLike() + } + } + .padding(.top, 180) // Adjust vertical positioning + .padding(.leading, 200) // Align icons to the right side of the post image } .frame(width: 393, height: 259) .background(.white) diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift index 4fe35c0f..a4b29935 100644 --- a/MobileAcebook/FullPostView.swift +++ b/MobileAcebook/FullPostView.swift @@ -118,7 +118,7 @@ struct FullPostView: View { }) { Image(systemName: "paperplane.fill") .resizable() - .frame(width: 44, height: 44) + .frame(width: 28, height: 28) .foregroundColor(.blue) } .disabled(isAddingComment) // Disable button when adding comment @@ -223,3 +223,4 @@ struct FullPostView: View { } } + diff --git a/MobileAcebook/LoginView.swift b/MobileAcebook/LoginView.swift index 881fcdab..82f5e7eb 100644 --- a/MobileAcebook/LoginView.swift +++ b/MobileAcebook/LoginView.swift @@ -42,6 +42,7 @@ struct LoginView: View { .frame(maxWidth: .infinity, alignment: .topLeading) .background(Color.white.opacity(0.95)) .font(.system(size: 17)) + .cornerRadius(15) // Password input field SecureField("Enter Password", text: $password) @@ -49,6 +50,7 @@ struct LoginView: View { .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) .background(Color.white.opacity(0.95)) + .cornerRadius(15) } .frame(width: 302, height: 180) .cornerRadius(10) diff --git a/MobileAcebook/PostCardView.swift b/MobileAcebook/PostCardView.swift index f35e3267..3b0ed028 100644 --- a/MobileAcebook/PostCardView.swift +++ b/MobileAcebook/PostCardView.swift @@ -2,20 +2,25 @@ import SwiftUI struct PostCardView: View { let post: Post - let userId: String // This is the logged-in user's ID - + 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) { + VStack(alignment: .leading, spacing: 10) { + // Display the username of the post author directly + Text("Posted by: \(post.createdBy.username)") // Directly access post.createdBy.username + .font(.caption) + .foregroundColor(.gray) + // Display image (if any) if let imgUrl = post.imgUrl, let url = URL(string: imgUrl) { AsyncImage(url: url) { image in @@ -31,20 +36,20 @@ struct PostCardView: View { .cornerRadius(10) } } - + // Display message Text(post.message) - .lineLimit(3) // Limit the message to prevent the card from being too big + .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 + Text("\(likesCount)") // Display the number of likes } } Spacer() @@ -59,7 +64,7 @@ struct PostCardView: View { .cornerRadius(12) .shadow(radius: 3) } - + // Handle like toggling private func toggleLike() { Task { @@ -74,7 +79,7 @@ struct PostCardView: View { } } } - + // Helper function to format date string private func formatDate(_ dateString: String) -> String { let formatter = ISO8601DateFormatter() diff --git a/MobileAcebook/SignUpView.swift b/MobileAcebook/SignUpView.swift index bc6ebd5f..80a264e6 100644 --- a/MobileAcebook/SignUpView.swift +++ b/MobileAcebook/SignUpView.swift @@ -46,6 +46,7 @@ struct SignUpView: View { .frame(maxWidth: .infinity, alignment: .topLeading) .background(Color.white.opacity(0.95)) .font(Font.custom("SF Pro", size: 17)) + .cornerRadius(15) // Email input field TextField("Enter Email", text: $email) @@ -58,6 +59,7 @@ struct SignUpView: View { .frame(maxWidth: .infinity, alignment: .topLeading) .background(Color.white.opacity(0.95)) .font(Font.custom("SF Pro", size: 17)) + .cornerRadius(15) // Password input field SecureField("Enter Password", text: $password) @@ -65,6 +67,7 @@ struct SignUpView: View { .padding(.vertical, 15) .frame(maxWidth: .infinity, alignment: .topLeading) .background(Color.white.opacity(0.95)) + .cornerRadius(15) } .padding(0) .padding(.bottom) @@ -109,7 +112,7 @@ struct SignUpView: View { .frame(width: 272, height: 43, alignment: .center) // NavigationLink to MainView, activated when signed up - NavigationLink(destination: MainView(), isActive: $isSignUpSuccessful) { + NavigationLink(destination: LoginView(), isActive: $isSignUpSuccessful) { EmptyView() } diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/WelcomePageView.swift index 69e9d7ff..8390059d 100644 --- a/MobileAcebook/WelcomePageView.swift +++ b/MobileAcebook/WelcomePageView.swift @@ -10,9 +10,11 @@ struct WelcomePageView: View { Spacer() Text("Acebook") - .font(.largeTitle) + .font(.system(size: 50)) .fontWeight(.bold) .foregroundColor(.black) + +// Image("ace") Spacer() From fab8952cd663864fd015289e536f683a25f66fe6 Mon Sep 17 00:00:00 2001 From: Karina Dawson Date: Fri, 6 Sep 2024 13:55:46 +0100 Subject: [PATCH 29/29] added ace image and image padding fullpostview --- .../Assets.xcassets/ace.imageset/Contents.json | 1 + .../ace.imageset/ace-removebg-preview.png | Bin 0 -> 20517 bytes MobileAcebook/FullPostView.swift | 2 ++ MobileAcebook/WelcomePageView.swift | 10 +++++++--- 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 MobileAcebook/Assets.xcassets/ace.imageset/ace-removebg-preview.png diff --git a/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json b/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json index a19a5492..787d1c22 100644 --- a/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json +++ b/MobileAcebook/Assets.xcassets/ace.imageset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "ace-removebg-preview.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/MobileAcebook/Assets.xcassets/ace.imageset/ace-removebg-preview.png b/MobileAcebook/Assets.xcassets/ace.imageset/ace-removebg-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..a134485d4f0b9cb1d8fe5fbdb8739661859185c0 GIT binary patch literal 20517 zcmeEug;$jA7cC4!*U%{;;i#n2-4fE0(nt)AbO}R?k|H1t3MgIDjes(wbeFV9cio5Y zx9(c^kGQPm0%YEL;+%c<-sgR*q4tarj}{LN4UJGqQBDgD4c+$c9}X7yAKz8;G4KuD zP3xHqTIm4&CK?(Onv$IK3vZL14BT|p)!Uw|syVk>gTEcHDW$M4s!Eo$Ia*&5IZ3o4 z@jkwUD~;8rS;#p?KLu z6W7y0pUmCE8;Nzd%u9}ii2L5m=&V7|xFBX+jqqx`Xc>qi8ZHxhFeEzq?mMfQ1_VBY zOa){&ZzR>Xa zYW{bF(zw}Dbfdhsl;9MKXqdD!cjr=U%Z6%X6K+xS;oseD5G301|KIli9qkU5adW)>EYpgao#OxrQ`%YE3jHX5iG}^Gv7*>ie*1Ajao-~i znO+zfGkORKxTh4^q4Xi#YKN)Ho%v=zo1c}laqH)J!Axh)Yd)Dc!5YssM=RF)(}-{W zHSX4gW+|JL>CBwCe^5r4>>_L!w&ctTt<|ID{21*l>lTX7(Hk(j8o_hGJ z){S#FdI-ekx`BbgV2MH1+Ns9Bc0R>69I#ZQ7cr}uSs@|V#HD-SW?RfE^cnVjn?DN0 zgs^!eH%(YqNvj*y7?#*Hnd~mVxxQ$A;ym9p!b}nQHcl!XOi9lay1u_HG}G^2ef8!@ z0dYR!0yk?=FAYZBoE@2uNbX~TM|W5LbuKJidr!-6*PZxKTUq8YjW#p#42*FMqaTN` z9E2i(6hZzj?o#ZudQd z_%vBAZVCbJk1#YmccXI4oE%c`>*EP+_U9>Rvb5ZfqqO2-9YI1xg?P7n_`7*rin1fs ztp)cKi!{hRrY$O(#H{;LFu9V8_+GpOYbBB&oqR)|tmi<6z1(G~V-!sH=@_4*P$$oK za``1KZEYn@O_B=3k{|1bVTn}lW&N5>5)u+*l$Djugfm%D2ZMa7>RBQlTUYTc&wOQj zambjF)MVi4cwIQS_#lUC{VFsH5j;kraa%)fSeoHV(i`Fhd3o{6%gd-3X<39? z!H2pzr$F`86`{!=kozU@sDs9Q1UILC`Qm3V>|yyQIiF&`#YxHGWJ^7RvRIQUK_2TC zEBgGc=wT4^*cyE&kXDi~vF#C5-T}iT(Alt0H$Pi`Z8_wU#j12O7v>`QWny)=Y`pCU+Yw*}kpKI`FJnj4Rl*~J`42~mX_v1;c zlmb!2=btP4S057+4*EvFX&+({(n6P%_hlgAOkl<12xgN=nJu&g4A40@RlMt2`6M;s z&aae^UcqEpp|AYP;D(Kj%}Vago0|r(cRwrkK*W+<4ZvlS4-Qr!-&Bn266;k#!)$1R?%%v7o2;;m5P$mg z&{LD#bl+QS0wy!L$h-cU%H=J=Q8HR2pS)%sLFZpLk&HnO(-wb~%aa}M=5X%RaPxUZ zG*3%rOjejWq$lQHRgL4!31!xx;M9(%pbmLg{_f2?d16{!&o<1*vWv?;Eh+h%*J=fa z^-WDpoRc47Y_{7fH9<1@5senZm4RcA4{-_Po{Rulw=zA!J_^t!M zd}Ym4SoV05BRU%%JtUMxswvmf)@ljPy2utew_cxkWDR;fPT#dNQn|IH89Xd_p(E}N zwkPpilh0wMTse*7b0Tn|UrRlqU%LneXNN3iY82H{C0)0BC{r0^DEt`gwccwMz!+BonXF~TnPP)y-(zaar$wShR}XJ0QcnZvyw zA1}P3&d)0>)RT&(@%^&rQ-27xe6`H>ZrJa__p7#dg?`DnsgBNdjb%>^i^S#Ms!oM+ zGDk(VyGP&DlN~LzpC~<+ygnNF6&lxgk0^oy@;9VSx=sVOfyxNC2vy$bu@a^yTHg1* zYJSr=SNb+rAFlK}9BdTvM&IKtS%q>6M@5OdldWsZj()Wn%+%vDsC zEh_Y%FfX(9Y z&wQ4auc?V?XQa+?-T%$jui0b;2*ksFvv6|uYZ)2!$qX@Hw~Zepo6XW6=`fh(bXmm9 zYbHL@D{ZQCUFp_tIbWkQnlm?=U<Sv#|+_w3|%I0&y7z56hvu#5y{)*4EY%U^N>4Rq8Ei+KD$xdKronW8Ci*u|pG-8E zcZN?eO89$|s*X5PhbvLYlgtdU*&c|M0q|r{GHwhep3&#;WQ8O3QEOlSpu6C5rwy+g8dsKCYK8x06i%$WRa|G+SW$Dka4`j1Wr^n>-=VMso!ku5TX#_rp zMs5ZFI)ZykqbG|IMXY4^Thp}yWXL^J>r7lEz9$2rx&!-!E4k3|VuKZGF>jYk#=t9A zQdA{?5Z`c`rE%y|{|6c3=B;pocsiAd18^CdhWJ!3RYs~iz($Pz1-`a_=czmlmV;2; z2WQ(^4!*;>>;8*KY!OOw@}?g=&$UzYg<&*xM<(WbTp~Zb%^|*nGy+vp91dnUt!_f} zVIa}kJklvV`rQLAUv{!H(-_Z8DwK6sw!FP%{tw+qhhWWZ?|Vz7+dn1flpf^vR>Y9Q zV#k$s-)rDQ_l`$@Kuw7!vH%k}NK{TZdeGyleY1Oe{cG_ zhfiA&I`M^n3l#^Y3Sod3lSLE}dgi)#FcUe57i;OlZu9c-F+}Tf{S=rYn&MQLjRopi znK#StxKEO-eBeE<{3^=I^qO$2DzVt0kxg<|rHd(xioP^H#BWh`@ndqX_;%gp2Y18T zuHLr#Ij7I%+*x5;Fv=`Ca4tl-cJXqD)0$L7Mhd^Yov=UwWJ;J=4^oX z8Md3dK_>)|p7K+h0s2R%*QQDdTayj~w7Pau4!BLZ>S($?;8&qU-8}e~SCW+^^^;dW z6KS4WlPk2kbF}}wgU5d}s3!%$DIPgD?PEcwca?Cy6~cQL3bKP-7Fo>*(qWO2*ldW@ z{6|p-*s3EcS#K^jaYqW&CGXMUmy@&HEv{TWi&rDE#Nsa;H-G1qm2=DhfKfQ)7GBql zEcx~3vDqrR$uNt{jpw;koz};dx{(A{&h7bTQ3u=e$G((i0}24yJHFbL8?NCpZWuMG zf4zz#=yca3538DTpfpIjR#{(!#D|*`-91jyJf=YTS$$_+^~Cl^+f>RII{p27Vqj8wcnFAPsfEyO9a@ndz8tY_z6j(>9g9GkV{fkfJBe;4mF`nhA5J+W z7q@iC!tJzunNEt3>+(u4lUO&;bMee|gUHqQIWmO*>X;7QP4fy0It>v@Bh{S*oqaw# z-zU~$>4kr%3Gqv-bOMs#9#;ianV3#v-v6+FIDz$ejIX4!%fkHVtiSvP^XHW7Ww-4c8snoM0vFr!SqQ6A6MO*q%+Rd%phkQGA-&57-@_{I^H z8;GIo!pCovXYOyhB@vtvUanx=N>B9cThGcvB$EoL-)>LA0T!%cD~(Sb@HMkL6|Ar5 zDyyo>46CgBHFR{a!FnFwKAx6JF`uPTm*Q=?9jMiyLIfyfj%>0z)zX>Nm@L$nNjtl5 z3^6~Dlx$_%w~L@OjRL5M^n2_*QBN082s+gST%N2`hi{Tbt~emIHK5XGejQ4Ii^4I7 z*l|6mI_Oajd`_={d+8KCt2Z=GOI~xn@nqWGW_PM;z0TD>BqCb(nBYwD_wz`ecAF9l3~sWS2W$>Akf`3&E(kIK!Y zl6M6qBDEr0yKyWeW05Fb)T=b$XB@LJxxCN#rn`0B+`mT}Y%rTTo>#WPqC(I^^QZV{ zFUjTmEh^IU5xxwceLm29Ciyl;_rY{R5*^3$Cun=I8&5&6Hlo9oMh zzAVW=81pN4JG;WJmHjWhp&_8 zGuSXEQ^rGH`JO+LK04n&b+qx#oY2#D_RDEnur2Pv0n{Pwzm zpn6m1B^aUd#Jcd85PczST9VU47QR|h_mZbQ{Uw&0bYlDI9>(o>D?cVD%I$H~@<%Hl ze}L>HC0_r*!Mf$fSofX_hTP*mFi*m_Prp3cblU+B-IM?2i;uMV1&O@vxY&4s8qvFEVf1p(YDZ47m!?wEdFady5s&lKjm+1*95U^7`7^ zbT~2No9(w`4@}gQl)l}aFD#h12ZcR~BJqRCfjPkTtq}`g-4|k(nJ&Nz*)K3VUg3 zX)Bl&m@=H-gC(8Oa4e5(WZ&-xq|(_rb8YYDoN8}PjEt@@A0 zP(S()pRLhvAv!wuK4oSOP*76phDPA)T1ov>f+4(rDyz7TC8E#Sz&~V{1i?FrjoQWS zz(Xa^>`0h@H+MDKoD{ZLvXO7?e3A zAA!ZdI_Zu^OiN(1VL7z7o6ReaF$PiPrA4cBGL4k*B*Ym;dm*9rm3zD|1G79EUJ>ft3zxtu3H0Ds zV5 zGI)%`t%gS1Cd|&mY|9_#@ZB)>%)jN=)YO#9!NI|RNBw>j9au1RIg)xVy1L^OfL~MZ zt-N9#JUpo~eLA(6`2fm+Y@CGEZ_H{$iNAN zkP#NUbt^HcrUA!ePebF-i6TNMq3!6w1S?^%J)Bzmo?&73N3`8vj(P1iQ~^KuG%qjD zg>qU&aNc_b`(=&8`5@vu`?5u7VXjoqFZ>D9z<_Q5Hq z?His>o&X%?Zwagh?F1XV+fV=Q1sdg%V6UeX|6H8=Iy4Um>>PVUYLZZlXinkQA5d&^ zig@ptz5TWKqqzY-#Oo;yvbo(oA(!l|!otrlbUcE&wr6T`)**D7W*i!(-;n6>7Jr$@ zC=0lkpvZ7%XC7%nSqG%EOVpU%b^rRIY8@c)sFqxwInd$YxfA%*0`z;3BOAd$1vRgR zM^T2G)3?8@3F%4S@oWrqLofXknG^C{Kar~!wy=7|mUba160*5lzc~vFGsf z<#6cNWi2c64QlhjfdRFC6pHQ#PaiBb-~Mrw%yuVD`-k8QrCDm6hYB(67B>vzLlI-y z#vQQ|4SD?+tQIGFT3&D8pH$eymA>>WUAq8;qiF8niUeG?GaI46-?UCkE)>)6Pp z4_U6jVb$P*NPie}q26(ohMBO2a&&guqQdyUq-kc$cDy)|!@wr04m=uuHV43M{(+b? z1+*Qv*>dA!n-HUkV%rADo2z9(T z+9Za_upf#Muk<4>L~PDR2NT`_!jqpAPKADr!+A}|c$H*2nWil{z!S^QV(6!0a=(6~@3BfuO41#AH=NA3^cJp( z7Hgo8E>+GWI}@fN`95jgr#2t4@HST{0anSs9A+6#4Z@%!KMRSiJaYYT8-fg=;Qtk4Hi^y-@~e2a(n!5;JayR{yPf!DG;7xAhV?uyVa za<0&y-J)YQX;>j5)w+xMJx+U<1A&T^^9s24s$&p%}YVfhMrxxS8Jz)`1YV>JEF3 z*I-my6B3%yi~{uxk+2Wpt9uN1?!t?60Os-oBH##Q0QDh0#}8JYK=E<&$x;7lRGLOFI+e33 zHRR$)?U>zh1j@=lSDa35qNH$3^e&nua=xrdS4-)o?$aROcp?I|aQO4*&r$i5!wOI~ zv~+ZI7buIe_VZ64_3A_C!=VJ~Ql<~?`42&>{ZjL-GS>q|9#59eIJXA;x;WnEy)dR} zdJ8eevgJyCmm%b8A)1Q!@NysN`pZg3C>lpC$=C33#Fm&nJa`ViNwn zd*Y2xswe~*qQN~%zrIHi&|h6xX!5SBQkq;Iw%LiRS0P0A9&#@qW4aoljQ|9u`lLfm zL9>aom5>Ne9|w;xDGvpDTEA!S_A!_3Sm9V=hNzbexmc47YtS<3eYoP<>v|3{_UAuq zrW^p>;HCYptHbR1IuDwK)(8nsx<;8u#sG#%nb*NP?v}H7)N0562J9sS&pk35@+ z8vvh`=0g8$+diH5i7^fu?^^`!282)?p-KLIbaa%ZtE-C)ls9LZYH9=Zb$Q;#Kyh`) ziDqGA%Zo28boLR;oN*AK!+y6PvhpW94wGFPlFyo*1!E8zgb7ss1m)TKRGYFoIVI(? zMvs*tCf<=1nw5=B!P@GoxOIhoH%QyV)>WE3yf~;$m)h)y{Me80VWD3`na@H^_}ra@ zVjd^4BFsHKYaVlR@0RgYJ~h@N-%zO&gW-JRM^)a&S=9@4@^}V#Elw>F0n`}WeyV!< z(=4o$4Q)`_)wQBkmdyK&T#^<3D+2~$uYa?FM6`T35h7d=3d{@AlAXe$(q19oBRGB3 zR4mK_zt-Hs7ihp-%4l*vmpXdJW9u8kpJCTTW%w{DWQ$@|5cS~gS&yU$PVgQB^B+8+ zV8WL$pb3+(WRu7!duK8VzWN#G1NP#n0>?UQFAnC#{ekByDn)uPUL18#)n3ho!X)1M z$LL5y6qwQIbctSk)ySFn^wj2rwhPWOg@gp4JG<}B&(-*!9}I-XA)<0ef9%XE*j3u9 zJRvcoUgK6Bu|D3J@mjk&KU7Kh<`n-GAMXegZdfwjAdt3G0_x9KGA^ZxLEU0N?5<-% z&yxwmOJhn&=viquIr53r$R5OiG_PBslIDQwG7}p2sIc%p%L__Y7#jrn)vMt9m5Ay8 z^#bVV+>y6sn*aqoiqX=<{lpnH=;KsnSmFT~2NL;;<}QmR9%?O_8hbtwX&ikH%1QID z`W#7&LNR`$0CKN=WLn6P!fwY&Pxu`Vdm_on$(i@{YouzNXN>iIWGGwQ9gP|lB^DYd zh($y%#PZ-l0o?pCnR%yXc1RGG?_ia@X5al#eTefZ4olBp@H9iX8-vvHnt1Y>3o|<> zCQh|}jHDvZKoL4b3fSr{$>UM&T5a;<%zVVnM>^^fB1SS+m?(sxGhK^(l_D9%Ke+n( zV|VGRm84L}y**XHg4#2PxZ8@!YtF%=3ZXBxc(pQI>9`D&#Gq!muuOiiUf7gGCIK~^ z36{Q4rWikmpQ=VSF0vcFdG%$n?zX?YrX4)W0{aULhRY3BOztzZmb_r8&Z_Q84lx(Z z1XjY{{{HLgXR!>*N6KpN(>1eknL%w&F;Q&rwAZ3SEC;TPDX)2j-D*KB!b{6*_~{IK zwBB#`Jqe)ICLm-J{2CnOR=i66ScU~3TDQhXBaxXWli&B1!6BpXq?B*)CnVTafaJi2 zVq-&0K+wK4NZQdeG;Cl=^!p$jy@}~NS#EyycdE*7PwaMUs!j2`R5~-q{iEEaA}ar7 zV62dG$Jr3KiKHMbAjgDUW&tU|nULnmt2;(-cKD$%?`!^Msm?!(>XO%dvgBQU@Tlb@ zUS6ey8_KO8E^LNxp`@`v^KRj4Bw~I{zxh;Ae5%gQL)q=Jl2?13>TmUmei}IVBH2S{ z6*Na9U|KIm_-0c`>ubVnu(|)_nf`%j2 z1-Q&=^v-Gty3u2bwh&~9waUXN-&U{P`IY&=+v|?dQVrqvg5RYk!1`PQH+Ym&SUAyP z#0>J7A?bV+^u8Pu(-0|GC}ZEW3+^Ww zGE9ouD*8zohojo+swpBKkD%I5=Bx~nP5^zE0A_`Aly3)1b)*<*C-#^6<{_jwq_zM? z`vLlY%yE4nqm^!S_G>CQpcItpm)Zqt0YFUk2sp+%$^JY&&f~@kBSkVH$6x4N7g=;g zou*3!_}VK9J}|hijpy#*|-b?cOz zhDXu>(EJE3c|k)cm?tH9dv!>`uAV6tu~IjXz$%RTN#raGQDo|cg7_NGiWrV#mQ_e| z)ysK^c~RQv2s7!LF8*!ueVo`T4f@^9361osHAiFJVRCVpjb zVkNJ6!w|@&&eGu*k473vgp}H=T(B}=kiy}e^R)$^#b)$h+?c5?duKI;%!TJG6d0J7 z0=te}R8W`2L!jy9&CbrA_6ca(iXa&P8y|MM5O@p3rGFo+rYOHDgq{O;4(uL;1LX_S zqSxMHJ9mm>XVfNCI0Lt5$bs>)rU}HTeyazDv#8G=ZSR!;X|<3e+5_W^}4s3Lm%=q5}Ez z^M)&W-60uCNlBZYjqBeEQ_&SGu0?Y7V%OcC^74L^Dl@3+j?c&tH`Ey$CYPV8ICaR+ zkMU4-fv^#D66ldzy$POR;W$)f8^WjLP~4p;TVRdKz0fAN>Qa4V{+;6ad+4$ZlS{e7 zL*`cq2;{v4VjKqY1Z-nvWSe@L+ZUl&xxjCJ6j7ldu2L`4O_^ z`ebIi%(V3l2C7o;XC?jPZhazv0alsNAgY>)>Y9n2pFVx^DghFfoN%Se*O&e9p#>fS zZNlV9wilxEma5N#%&B3v(n+hI?{vAacTyz6WPAyA9 zrL=0K>{SI7PQT=sc$DB@3w=36MMa--a})aw{rY3p%mpsoqXVS45Ty=4UjEuJFV=oS zIuvqZpXyl*;@2l3tc>?2?4j=9Bl8EVC_zBoZJCr&O-MiZmQ`W+OIB*(t3~aM3iyik z1WeTm@K>p`s}ph?lW9jwpZ=XX>}69)@^`G7vh!{>8Rt;z_PvjkW6Q>`?zeXHYo3>l zmAJ4yiC}zlW|KSqz^Ds50$6*(QLDX{1~n}lsFTUWs64RWHFO~jQdJ!I?Flu}jFK&ls8tc; z@hj3D3GL(TJH^rN?ktiIqwI$8-Wc=cTrf@dEypgB>OBZyUu9jd?cn{~p$JWL@ z0uTP!V|V^qTwYT|o4ji|Ue6k_rayT19DK}juc)n;z$YRW+y=!#F|2vd_hDnL(_E_W zjAL!{Bb<&EKGl~zW`R$9X|@v`ppg4J06RSe`14!~l-)mG6|Hn_Sr#(<7Y~7VL45nz zw`Z8ov2$nse-~kg;Y{xW9~-x?Smx$iqhuFH!H-fH>4iE)Cl??#ue;DEN2Td>IC7;h z;Qq)&>v5G!_>z0~*K6?CTM9q?Wxd=@Xait~1pfl>2U%+VHS;r9dXYEL#3~AqWEQk$ znb$7L3ak$s@;*4@BLhc2!U zv?9rQAk0nyE5N1j)RyhK;{+o$|q#rC-*K^mWXS(0h z9Jbk)HJ|9^j^&7*q^f0`f18_1p=X52h-6D87i=yzD>7X`gQ32g9bx!czJKDMMSV6c zbXDfbkOD@usicn|`76GBc?Fl(6pN9jE8xv#Q(W|iAlW=K0cnuD-k&B|0$^6uC-^6| z?Dl#P*mw6=VkHN(b;c0vHkQWmQuj%-5e?ZU&Az9M!ms~q%T>3E38ZOd&wRJ{ItI3& zapipk(b!kAZG&?J7G6jO&MH;gL@ae3`o2xSku4ELSxq?S6xXxYfjRFKDKA-Bg+{n=a zudqh8>Q^j%t4FK>*nWo+qm3l)6$#qD$cpx;Uv9I8dlz4t(qE`-I7i74TL` z%H?H4e~wm*+wS!P%MM-3;ee<;0F_LkJiv+(MEQ=LT^t!CC982$mg#@+Yre{6P(bI3 zkbW5lCggwSVi$r%NVzYT#fAgHzPwoq#)4>8%MJ5k3$k9z#!~Qcp(j~gcfQ+uh!cFP zt)mlLSXc&jgmd^OH4WhfmHdCw81IufU!I(u%s3YSQy~T|mRxN}ckVkrg~q4k7uer$qD(MG@Q;o*N4BG>c`evfN1{PhaC9~# zG^=@{5ax9sse!ve7QEMliOZM1c-;jO=4A{m|JIAIK5UjE;7DNf;s9ai)5hU>W;Q|u zGo~F6iW^a8@N2`Zn2eN%9(_Zp6LTzSr3DnyE|gqjRM5scg-z7ej~z zOdH%c4KvqAFr~}4R)%=k341J-{s-|vMb^6R3+arKFD%Tg1H8Kkw`t2&&qiXZP<^fx znM}*o0ZJKUD|{U0d(4rNOstuO>dL#BcUuXA=tR&I#~+*=vSrKZklrojT?%y zHj!1|9g;dCQC@8-;W(VhA^nnyE&N zAb_9D4@SWZA42Nx4T$cJ0uJhQb8Sil_w0?`kL&G&w^Zop!tLZ^Uc{(n+o_ zPX%|T*Uf&zhot#Z1)Xm;+C#8yloAq&z?E+3=ORemo}c|8Uc=x43BH<#ZP z*X|^t?4-SRY{t=lfS+@+v)j@EEXKzY-N*aqi&Exy*ew!p3^XdLs^SJfgxNhmT$hY# zUcZe~KuVO2eYYK6&bf<;F@=TS$mNIY7({4e+1B1dA>u#WK7_z@kII0RM6w!`B+);y zFfVc)7dwe1o0b7CzXk^4@nEJe=9bbReKkP6GjCO3p5c7b??>$WrZN`mk$@byZEx5B zs=nGoPR<>9oF%fEE?aR@9uD+LHhtULnP<+Euth6$ZnHYz@_Me8@xP1nNn?ML!9Jj?+j9g=LcoHBadrQlq zl%SxQ2xHNQx{=}!G`Xq~c+oxb~#>Bw*5)tI$TaU5o%)cnu$oho9=OD};- zYb)00epIfcSxi16B2~b#VeM>x<&=tw%9L>5ySxE`u z1>k7-8BYZ6Uelk44Eys$3DN-ia(NpM_Is5T9WID6*o0mbR8)iXz#NH}j0#i9>S%ilxrfCe zzWFnrZF=tCurv9!DV8an+0{0L% z4l|ABEF9WNE6Me;=I=jj2Jb}|fF11uPNr-ySe0lMag;{*`b}aAmr6o^0{tNy*cLoM zG4(iKEn`a>z65ObM6nYK-y#U>`dJ_78@HipPZ~B~*CJr@ zC7`H{ykd;B)*ZmmiAakmkqc9zvZTr-uLUrrem^ zFtTb1bO4MNzz34;qo{8Ds%hU$6)TlDc1P3cxdEX085mtmV|zaIR&o&P1ij@Bki{>B zFv*$P6!(mWdkjissgLvZe@0ACY;JCjJWx%owgevDo3hq&yL@1z{S276y{3G`09+7% zFj{b4$OWL#cgL?y{C2+M0Ses$UtTkOUo5bS2}j3KfQ~aN_cjlIrSM{ltcr0fyX-ew z7KE|xl|j;g1X&LdW~#W7xADPjS`Jt_{D2QxK?@cDVPDt^_C1Dh%=S7k<(l>^p@&QT zxxg?UxKDcFX@{@zGO=j}>eKmzM^5diO5SD+P263Sga-QZ#W9U#im`=Lg3 zT}y8hGN%t0yed!E1C>cyzc&v%2TKoqy z!XeKWSLyVJ{{TySBb!pH5MY!&?(Al@s$^wPz|3=z)!eoqZm!OCE>3qBfQ^9oqt!W{ zQo^FWutZz=XxGvnKCReKVB-eFI`FZLg8=O{UE|p328!Z>K`-?8l6Nkk9C*}(Qi8ja z5wmY{$KloKJ1Xv$^_$Dx+s!KrU^ddOb(nf>Y;L|}7H;lzKXlZLIvknI?Vm?%VvW#k z2i+~jLaRdm)WgkdvIvJoeSjfSG|PAE;jG=u0nc|3W3`}*9pJ$%-uuZCJ9{@c(STK< zEgDmn)$O3M<19*8m~oINx~(5-O$O7dqe-b!GJHbM0{G(L_bk`VhV64*5T)$T_2?qnWTk^8@ z&1=xcgW|@l1t!`7P&bSLe{^!c^)^r=UIUWXWh*A|-;#`8!aM#SZd_qhWrc-RASl#5 z0`<81O3Wfz6p$`ce09MS6(00G9h@ydOp{e|2ogcBTQ_9sG0B!Q*u?6FL_MoGE~7b6 zyL^t*?t{R1!%cT62>e~v5k&VSN?v@^nhOqw+-hj@aqCAIYiZpFHG4X+5R0e9$GcAP zjsTl`X)i!SX+SMt4rZEF#zAIEaQwQ=T7Rg7X?W^P(#ewrjQ;~Kotr&Cvxi(uUar)$ z%x{uC!0bQH`})t%BH%@46sJ2qF_%%ylX4dM^l6)z>v@f%B%P6+CLlIz2pGi9^pa5& zzxCz;Wh2NS;4n!=(D_0~=VNSG8?bP#_`K=7+-+Sb0M%M@z}uieW3FT?uyV%k^rweW z2h_~aP+H#+SkbC8V^7BooPX(^)pp=M*43pactnBK^>wNrlUO6<$$b{)I1dIgL~9R# zkM|{msAq9&=aTK#;W;hlc#PrEP!dupsr3OxNC-~`Bb9+Vs3}FnGoQTBzil@0N%`@6 z7rB6mAI|WA`RoQ?dSqnNXVa(c2cuCX$K@hm4oCP@1KFaMAJEXyEJ&P8TDFEh>hrA! z)P3!n^VMX=qC!K8zcHT0AJ9xc=5NYkU=eJjfB4|WmmZEGa~&IawGstrH2&{X4$W*z z4f08=j|e(7022^A@qGIxSY5m~ZQwu#SsQ%6?V2iL@g(JQ!%8EDO5+KQU>?LOn+AXrDBM+F$U4%RP5$#<$*C5ReB1^_%VvRb#l7a z`F0c(gxyaGEJL1H4r35P9UeJ=)j#0~)KLP|hSV}}hVR$c*YkrzW4l2^jYQc*+Bdke z2&%FiuM5lmPY&1OApX9DshP(Kl``E`2VrI?(M_{S#3B{?mFSsO5n}?HeSL`Op9__E zbLz5wYf)`OLp<)Zl1h$s-NF}$6#%sT?Lf|}llja+$kyklk=+T^o&N(yy+pu%yx1Dp zY)+pLj2HlK|Iq#zU-Ssy&Mt1(p&ze?(_W`X%RMSzp%6a@sw&aC2-?}p@xVZnPduSZ zVf?7n{EtrvsunCNijRDow`qZc{E%4k$|W@yuBe@FKUSzSc1MiFg5JI{Qlnz1h)Zw8 zgj21$1aPv^<-C_QEe-|>?CR^+uYY^~oBg!pBJ@O@L0wNqkwxDXXIGE9D4gl#-j1xK zIQtoW3*1MI_}{betDk<%g#(&<(&%+K>>#YLFbizpU2FpCpbn$_ zB;%7bjcn9FsgYNQ+%njrI;Q}5l<4U6vxf4m3vn5b@I~?y%JkoZp=>D4r5vY$vlBB*g-G7xd5keXg#)-~|#?)*Xw=LTcH* z`g+W(-YCnB-YYB@equ}Pyg@|ZfQl&F&l=2`aKgeFG@@=m@i$YLw#*w zKxxuq15DvaSY^^mUBAcnxVQiyCM=8u^h$f0`vgBrCDRzca_Z~v|6XF!EaC`|*mrz9 zye(j3j9IA@$c2xCjs+~)Mr0Tpf-TTIoF z`|_H5z((4Ozk(_(>?mz535^p6;?8+J2$BV$oLlEocRrUna%$MjrvusVfETBl1I3HK zibpRLa~PR>h6*8;nDhY#Z}Vtb(^9seK?VRK6IIg$bj03Vd3$VVj_}Qeo2MiZbhar| zJZr0^OrZug@Z3v-UBtF5Q+pQG1*OLgyB z+IP)^ixcw^K!5`aO{P58GBj=%mswKptHzQy*vNr{eSPl04t-FmGSrJ_34EhT0M*2Z zH2!hqP#qzHCFwgH4Ev>UpzNY@(|`T^xp4_d$1)CkahZCcvrY**&DzdX*{F{N2M0gK zVfMgfE~h6m8G>Ew)zC*_TP9;+ig*@xav^|0qXy*Z z17K%TnrjUV><5Uc)7yj4JDVo_n*z=^tbJy{igBP`Hxw2cWn*I+5?D@O$l~P^k@@cF zP=>QaaZ;il3N?+aA6`pqj;h;yuD#xL5mEyZ`0k(M3DaG~d{aGl(n}BX@2G6)kTnf* z590S-JzGsZi>|(^BiVw&qLPw<{-F8r`mwf!D`@F#Yp|c-LV?P{@f!Hd9#K+I=m6U8 zk?M$!PiEOerQZ~}aNS`(&J5UcsPTK`Io>VeDS8(EqO^!og;(krK}uv+WWXH(I6>| zOg<5SpHXIOG&JI#yY~Y4XGsK<8a8?FlZKnqcJ@+w*BaSw8hok8E(0`P zp_IwS9DaR$y08gA$pz?E$^j&(x{8w0+p6kn^6s*d zV5T6AOTUSzTq$BhYwGUZZQwLXNK7Q(s|UihY#yj-r$LRFb85V%H0$!Wr#c?AUEiaL zNl_8TeE~wx<*yG97Jx7i)zVDYZE8s}pdCmelrf-HJ9BLVILAit^cdmZpl8SR&daZN zr7r{n%8KJOkN{4jW_H5MOLSA}Mw2tUUenXE8?z}rD%AYkO$HuA-}5BSOh|>8n-wMq zcAl$jEaoF@1oJ{SVF?i$bxcJV-UM6V5xoH~%`$ojL1z|p95wmF=we701Myo<%S!M#2Z z1h(>2%>U=A7)X%P3_zpI!TZ5=IOrld4CGtwJ0 zp~O50;7IFB;r%=fR=~n%_#=FGnm8$9k#W@DY@>j$oty5Nd%n$CcTKW=;-|A1+Yz9( z)b(yuss8GJRm%of>%S{~*h1sND>*tzrEhBv^|`9>xgNV@zKRFvd$iVh;gX6&e-Gam zANR*Q-~np-X<7dUqE_Hm4v*q~n~U=@``U&o*sLQ?mLqAZ9Ur|nL|tDT;wlW44*K3v z0P22UV9mQkkRJ$a6fPfBTY&)rLdnUAToUlQr%SS|1i* z`ROk%E{=qT;N>|y4}k^))4ru&Bpot>!T0MvPz951Q^yHJ2C>L&Ck^Ude?*d87pSI% zchbAsv#oJaV@}WB#J=1y7d8geLIV{g<-l(~!sVzRtU`fs3ZkQI^Or|&L7(%HI7s4p zIkORnIH@n-3eQsEYgD92ATLLW`}u&}Kk>Pnf{KLS4~IUt#Kfyc7&)d#=>k8JHmLfKt8RJYUu=49Zole ziss=Jcaeq5KOfY)?weC8a=&dRPD&r#V`oR$Ta@yD4er1;0Nxw0ORl41kKAbI)mEU|Xyesq z8iL(ampjchu(-W`E#a`xBJN<(j*lz%5v+q3`xa{=NTm<##=K6mLR8FOYC%Dtl*F!J zi+}x!Dy?4ubeAe>j$QzM`g9XW`btM!q>%z(^lizyudboApvGDQlF1=p6@ZC;$?L+r zL_y(;I%%*5a7k&!_PDK$zjWTI1fYCQp@z;;<$pcd&txYo0KSg_jhzx&z?ufS$svE~ z-;c%Xu3~=fSq<`ETmaN)u~uF$9q`y>`hoszGp5@ENcSbT5_k;?o#H6&5%5n~wg+Q6 z10FR91*@$c6?bpsu$a7WD*f9DpR&{lItY~5h zbS|c=Yrf#5o*(iXy4aI;D2Bg$=u1q?&W|Y_WKs>@CQ(rYbn$)TMo)95+e-7UMqsG~ zPDl82T>%L4r-v+XbWCp_>teQtFzPqwsJDIOT%VlFW8^3*69Q#Z>(9!oK|or!y5H#) zKL$uSemUH&z_oqq2|-cO%R*pZ@ybxMv9F!UfLLHtMP_IYFg4MW0)oO{BI~w`G-War zO?g2A^lhK^rwbphqvm1*pFylZE-}`9?Y&@y+cv!pTr%58@NNeLeUDBO>o0DXa_O)Y zA;_)Z1!l8;zG*g;nmR<&FjVudUlq}6Lhgz5B zu@GlVEmqoB4a7!L9%jqQ+9WNPXeX|RusZa*sBOmB%v$5MV zJR%~02B7!W$d>Yf-YF>vy_@c=$dm(yFTFRKQ#sOTMPx|Nw6hAGJv``4Z)omc{RnOE zaAr)W(MWmtA!r+V$`yBD(mDt}3_LkIdVL@-<1S-&JbJ^8dYHK=U=GJ6c8(W45|QC} z;1@=`d@_jWd9EZXJ=plY0>68%$7QbP8}h9ElpB+;5*0e=@Pkig1h%ZKp%M>?7?KE#_2r9bqz5Yn)dzn zw273ReRj$vo&M^cf@2qTU_C=A{s9W;#^_jomkGZ>x~L>?Ia5;8J>axFRlK!d%JQ8@ z_#hRy&$UTcKf|?rr0I|>i`qtr`sJ?7I1CK4*Y`>mZ#PGWOP3`O5=D(={;>PI{kPYa{zWdy$q8Wt|2f z;xC*E3oe_E<1Sum+N7?T4k17bxt+xdRzqhn5B(B<`@~&am&u457VGqCsJ4DdWflVD zesYX*smkwOT>=WTk9v8HCOVUfR_nZ)02c$9h(ZRhP6g%^hdSDp#-updw|X3OSRb&P z)!EhcmAInf`l(vOZK+iDhOsi|Lf2u9vnZ<|qL5d5&b>np%rw*RH=YyM4o^MtOio;Q zx~vJ_Z+ZW9a-9Y-If1D8Pts#;gu5ynD6b!TZeFiB(%p6x2E*;bm*%3e*O7d}iQxau zy|9A&tM#NWnRC2zCx}(D-mKkw?FJ&(-=~p{K8HqPh;#k-opU#^w;HU*B3`oG-z3UN zD{a2HYx&Ig=X2-MjZlWPs6;MirvP?B2m>Ld8e-jU+B_e)s{9Ga3lfQ>m5#2HlyvYv z*jq__$QiGXnh9`mrmuw73E!MO{j>xopk3UHP>>};C1>Y|_cmjBP%8ERusPq9L<`+s zJVMb2)39z^R%JA%sYm!h<6oYgJz#rwVpA2C?~^BHb@De&XI%F3@omT^OYg@IqLMoLM$BlnYjqn@Xtz$d$JB z*vA$;Ye(;gxl<|^m!7~};K>;X?Svg9Z(BtwE?v)%u5#D1i*29TDN-*4V5U;VPotbh zy#PgP0?N%kJDZq$p-53m40jx5fx2P`LV~Cl*RDOwd5l6Weanq^F{YHL&i1>;Vig=k zGf)dbRj{(?aSYJX$^auXwQYH@u-JFt6jh69S{CcPGd=U! ztuW@PQ06Jft=Rb}{+!-yXVSi&>)K!O#=QEa!85K=fbCj6PFr`zeA8Cr9$Vb95lXG_ z-|%y6$%I?@Zz^$Z+oX1}Xpe$`-Y<{6R0B3e^5n~6!!jW=jwwlA6jWIx2fCLAP-Hfa z=9VgsqT>WMf#C->b{9~teoJIZrTkV7Y4H*q5IIh9(SBB`?4C*<^=5S$=Mq{;%GEUlK zGQ;^p63jTBh%n$wsVCrm=|BfAE&xX~2XqBw_{Mfp@cR&3kwmu0O#~3Pg|*;sDKPMm zkPu!Fos2rEg6E@)ZoL;$lfz1o5JGz-FY|6E!7~*#jBNA@JVNraux=MZ^b^!%^84TP ok86$>5+Vr{k^lXzf4N#FPhnMaP)d>OK@0dW=sUJtq9ySE4=oLb8~^|S literal 0 HcmV?d00001 diff --git a/MobileAcebook/FullPostView.swift b/MobileAcebook/FullPostView.swift index a4b29935..7f14dfc7 100644 --- a/MobileAcebook/FullPostView.swift +++ b/MobileAcebook/FullPostView.swift @@ -40,11 +40,13 @@ struct FullPostView: View { .resizable() .aspectRatio(contentMode: .fit) .cornerRadius(10) + .padding(15) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.3)) .frame(height: 200) .cornerRadius(10) + } } else { Rectangle() diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/WelcomePageView.swift index 8390059d..4aff8fca 100644 --- a/MobileAcebook/WelcomePageView.swift +++ b/MobileAcebook/WelcomePageView.swift @@ -14,15 +14,19 @@ struct WelcomePageView: View { .fontWeight(.bold) .foregroundColor(.black) -// Image("ace") + Image("ace") + .resizable() + .aspectRatio(contentMode: .fit) // Adjust aspect ratio if needed + .frame(width: 100, height: 100) Spacer() - Text("You are not logged in.\nPlease login or sign up") + Text("Welcome to Acebook!\n\nPlease sign up or login.") .multilineTextAlignment(.center) .padding() + .fontWeight(.bold) .background(Color.white.opacity(0.8)) - .cornerRadius(10) + .cornerRadius(20) .padding(.horizontal) HStack {