diff --git a/.DS_Store b/.DS_Store index 7fa1be84..aa449668 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/MobileAcebook.xcodeproj/project.pbxproj b/MobileAcebook.xcodeproj/project.pbxproj index 5506db3b..89d2039f 100644 --- a/MobileAcebook.xcodeproj/project.pbxproj +++ b/MobileAcebook.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 35AE2D022BCDDD9200EEB0AD /* PostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AE2D012BCDDD9200EEB0AD /* PostsView.swift */; }; + 35AE2D042BCDDE7900EEB0AD /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AE2D032BCDDE7900EEB0AD /* Post.swift */; }; + 35AE2D072BCDE06500EEB0AD /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AE2D062BCDE06500EEB0AD /* PostView.swift */; }; + 35AE2D092BCE8B5300EEB0AD /* PostsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AE2D082BCE8B5300EEB0AD /* PostsService.swift */; }; + 41EE72282BCFDAE9005AD909 /* SignUpPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41EE72272BCFDAE9005AD909 /* SignUpPageView.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 +44,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 35AE2D012BCDDD9200EEB0AD /* PostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsView.swift; sourceTree = ""; }; + 35AE2D032BCDDE7900EEB0AD /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + 35AE2D062BCDE06500EEB0AD /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; + 35AE2D082BCE8B5300EEB0AD /* PostsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsService.swift; sourceTree = ""; }; + 41EE72272BCFDAE9005AD909 /* SignUpPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpPageView.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 = ""; }; @@ -81,6 +91,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 35AE2D052BCDDFF500EEB0AD /* Views */ = { + isa = PBXGroup; + children = ( + 35AE2D012BCDDD9200EEB0AD /* PostsView.swift */, + AE5D85D92AC8A337009680C6 /* WelcomePageView.swift */, + 35AE2D062BCDE06500EEB0AD /* PostView.swift */, + 41EE72272BCFDAE9005AD909 /* SignUpPageView.swift */, + ); + path = Views; + sourceTree = ""; + }; AE5D85A32AC8A221009680C6 = { isa = PBXGroup; children = ( @@ -104,13 +125,13 @@ AE5D85AE2AC8A221009680C6 /* MobileAcebook */ = { isa = PBXGroup; children = ( + 35AE2D052BCDDFF500EEB0AD /* Views */, AE5D85E42AC9B060009680C6 /* Protocols */, AE5D85DF2AC9AF83009680C6 /* Models */, AE5D85DD2AC9AF72009680C6 /* Services */, AE5D85AF2AC8A221009680C6 /* MobileAcebookApp.swift */, AE5D85B32AC8A224009680C6 /* Assets.xcassets */, AE5D85B52AC8A224009680C6 /* Preview Content */, - AE5D85D92AC8A337009680C6 /* WelcomePageView.swift */, ); path = MobileAcebook; sourceTree = ""; @@ -146,6 +167,7 @@ isa = PBXGroup; children = ( AE5D85E02AC9AFA9009680C6 /* AuthenticationService.swift */, + 35AE2D082BCE8B5300EEB0AD /* PostsService.swift */, ); path = Services; sourceTree = ""; @@ -162,6 +184,7 @@ isa = PBXGroup; children = ( AE5D85E72AC9B29A009680C6 /* User.swift */, + 35AE2D032BCDDE7900EEB0AD /* Post.swift */, ); path = Models; sourceTree = ""; @@ -306,8 +329,13 @@ files = ( AE5D85E12AC9AFA9009680C6 /* AuthenticationService.swift in Sources */, AE5D85E62AC9B077009680C6 /* AuthenticationServiceProtocol.swift in Sources */, + 35AE2D092BCE8B5300EEB0AD /* PostsService.swift in Sources */, + 35AE2D072BCDE06500EEB0AD /* PostView.swift in Sources */, + 35AE2D022BCDDD9200EEB0AD /* PostsView.swift in Sources */, AE5D85B02AC8A221009680C6 /* MobileAcebookApp.swift in Sources */, AE5D85E82AC9B29A009680C6 /* User.swift in Sources */, + 41EE72282BCFDAE9005AD909 /* SignUpPageView.swift in Sources */, + 35AE2D042BCDDE7900EEB0AD /* Post.swift in Sources */, AE5D85DA2AC8A337009680C6 /* WelcomePageView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MobileAcebook/.DS_Store b/MobileAcebook/.DS_Store new file mode 100644 index 00000000..16973dc8 Binary files /dev/null and b/MobileAcebook/.DS_Store differ diff --git a/MobileAcebook/Models/Post.swift b/MobileAcebook/Models/Post.swift new file mode 100644 index 00000000..ddf6a56c --- /dev/null +++ b/MobileAcebook/Models/Post.swift @@ -0,0 +1,30 @@ +import SwiftUI + +public struct Post: Decodable { + let id: String + let message: String + let createdAt: Date + let imgUrl: String? + let createdBy: User + + private enum CodingKeys: String, CodingKey { + case id = "_id" + case message + case createdAt + case imgUrl + case createdBy + } + public 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) + imgUrl = try container.decodeIfPresent(String.self, forKey: .imgUrl) + createdBy = try container.decode(User.self, forKey: .createdBy) + + // Decode createdAt string from JSON into a Date object + let dateString = try container.decode(String.self, forKey: .createdAt) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + createdAt = dateFormatter.date(from: dateString) ?? Date() + } +} diff --git a/MobileAcebook/Models/User.swift b/MobileAcebook/Models/User.swift index ea748dd0..bff76121 100644 --- a/MobileAcebook/Models/User.swift +++ b/MobileAcebook/Models/User.swift @@ -5,7 +5,9 @@ // Created by Josué Estévez Fernández on 01/10/2023. // -public struct User { - let username: String - let password: String +public struct User: Decodable { + var email: String + var username: String + var password: String + } diff --git a/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift b/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift index ae012f49..7b489c9f 100644 --- a/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift +++ b/MobileAcebook/Protocols/AuthenticationServiceProtocol.swift @@ -4,7 +4,7 @@ // // Created by Josué Estévez Fernández on 01/10/2023. // - public protocol AuthenticationServiceProtocol { - func signUp(user: User) -> Bool + func signUp(user: User) async throws } + diff --git a/MobileAcebook/Services/AuthenticationService.swift b/MobileAcebook/Services/AuthenticationService.swift index 9f7181c3..ebc5350e 100644 --- a/MobileAcebook/Services/AuthenticationService.swift +++ b/MobileAcebook/Services/AuthenticationService.swift @@ -5,9 +5,28 @@ // Created by Josué Estévez Fernández on 01/10/2023. // +import Foundation class AuthenticationService: AuthenticationServiceProtocol { - func signUp(user: User) -> Bool { - // Logic to call the backend API for signing up - return true // placeholder + func signUp(user:User) async throws { + let payload: [String: Any] = [ + "email": user.email, + "password": user.password, + "username": user.username + ] + let url = URL(string: "http://localhost:3000/users")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let jsonData = try JSONSerialization.data(withJSONObject: payload) + request.httpBody = jsonData + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw NSError(domain: "InvalidResponse", code: 0, userInfo: nil) + } + if httpResponse.statusCode == 201 { + return + } else { + throw NSError(domain: "HTTPError", code: httpResponse.statusCode, userInfo: ["message": "Received status \(httpResponse.statusCode) when signing up. Expected 201"]) + } } -} + } diff --git a/MobileAcebook/Services/PostsService.swift b/MobileAcebook/Services/PostsService.swift new file mode 100644 index 00000000..2c23077f --- /dev/null +++ b/MobileAcebook/Services/PostsService.swift @@ -0,0 +1,38 @@ +import SwiftUI +import Foundation + +class PostService { + private var posts: [Post] = [] + struct PostResponse: Decodable { + let posts: [Post] + let token: String + } + + static let shared = PostService() + func fetchPosts(completion: @escaping ([Post]) -> Void) { + guard let url = URL(string: "http://localhost:8080/posts") else { + print("Invalid URL") + return + } + var request = URLRequest(url: url) + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjYxZDA5NzZhMThkNzVjNjE2Mjk1NTA0IiwiaWF0IjoxNzEzMjgwODU3LCJleHAiOjE3MTMyODE0NTd9.dsMSLopZqaXqNvnIK6YNUebXis1lTGIfwpH65jelcNk" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + print(token) + + URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, error == nil else { + print("Error fetching posts: \(error?.localizedDescription ?? "Unknown error")") + return + } + + do { + let decodedResponse = try JSONDecoder().decode(PostResponse.self, from: data) + DispatchQueue.main.async { + completion(decodedResponse.posts) + } + } catch { + print("Error decoding JSON: \(error)") + } + }.resume() + } +} diff --git a/MobileAcebook/Views/PostView.swift b/MobileAcebook/Views/PostView.swift new file mode 100644 index 00000000..909da845 --- /dev/null +++ b/MobileAcebook/Views/PostView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct PostView: View { + let post: Post + var formattedDate: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MMM d, yyyy, h:mm a" + print(dateFormatter.string(from: post.createdAt)) + return dateFormatter.string(from: post.createdAt) + } + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("\(post.createdBy.username)") + .font(.subheadline) + .bold() + .foregroundColor(.black) + Text(formattedDate) + .font(.caption) + .foregroundColor(.gray) + Divider() + Text(post.message) + .font(.headline) + Divider() + } + .padding() + } +} diff --git a/MobileAcebook/Views/PostsView.swift b/MobileAcebook/Views/PostsView.swift new file mode 100644 index 00000000..4fa972b8 --- /dev/null +++ b/MobileAcebook/Views/PostsView.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct PostsView: View { + @State private var newPostText: String = "" + @State private var posts: [Post] = [] + + var body: some View { + NavigationView { + VStack { + Image("makers-logo") + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + + ScrollView { + VStack(spacing: 20) { + ForEach(posts, id: \.id) { post in + PostView(post: post) + } + } + .padding() + + }.refreshable { + fetchPosts() + } + + HStack { + TextField("Write something...", text: $newPostText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + + Button("Post") { + + } + .padding() + } + .background(Color.gray.opacity(0.1)) + .cornerRadius(10) + .padding() + } + } + .onAppear { + fetchPosts() + } + } + private func fetchPosts() { + PostService.shared.fetchPosts { fetchedPosts in + self.posts = fetchedPosts + } + } + + struct PostsFeedView_Previews: PreviewProvider { + static var previews: some View { + PostsView() + } + } +} diff --git a/MobileAcebook/Views/SignUpPageView.swift b/MobileAcebook/Views/SignUpPageView.swift new file mode 100644 index 00000000..ac05f81a --- /dev/null +++ b/MobileAcebook/Views/SignUpPageView.swift @@ -0,0 +1,70 @@ +// +// SignUpPageView.swift +// MobileAcebook +// +// Created by Fara on 17/04/2024. +// + +import SwiftUI +struct SignUpPageView: View { + @State private var user = User(email: "", username: "", password: "") + @State private var signUpError: Error? = nil + @State private var isSignedUp = false + var body: some View { + NavigationView { + VStack { + Image("makers-logo") + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .accessibilityIdentifier("makers-logo") + TextField("Email", text: $user.email) + .padding() + .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) + SecureField("Password", text: $user.password) + .padding() + .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) + TextField("Username", text: $user.username) + .padding() + .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) + Button("Sign Up") { + Task { + do { + let authService = AuthenticationService() + try await authService.signUp(user: user) + isSignedUp = true + } catch { + signUpError = error + } + } + } + .padding() + .background(Color.blue) + .foregroundColor(.white) + .font(.headline) + .cornerRadius(8) + if let error = signUpError { + Text("Error: \(error.localizedDescription)") + } + NavigationLink(destination: WelcomePageView(), isActive: $isSignedUp) { + EmptyView() + } + } + .padding() + } + } +} +struct SignUpPageView_Previews: PreviewProvider { + static var previews: some View { + SignUpPageView() + } +} + + + + + + + + + diff --git a/MobileAcebook/WelcomePageView.swift b/MobileAcebook/Views/WelcomePageView.swift similarity index 100% rename from MobileAcebook/WelcomePageView.swift rename to MobileAcebook/Views/WelcomePageView.swift diff --git a/MobileAcebookTests/Services/MockAuthenticationService.swift b/MobileAcebookTests/Services/MockAuthenticationService.swift index 29a608e0..a4b11e80 100644 --- a/MobileAcebookTests/Services/MockAuthenticationService.swift +++ b/MobileAcebookTests/Services/MockAuthenticationService.swift @@ -6,10 +6,9 @@ // @testable import MobileAcebook - class MockAuthenticationService: AuthenticationServiceProtocol { - func signUp(user: User) -> Bool { - // Mocked logic for unit tests - return true // placeholder - } + func signUp(user: User) async throws { + // Mocked logic for unit tests + return // placeholder + } }