diff --git a/FakeNFT.xcodeproj/project.pbxproj b/FakeNFT.xcodeproj/project.pbxproj index e1ee3e6796..de49900131 100644 --- a/FakeNFT.xcodeproj/project.pbxproj +++ b/FakeNFT.xcodeproj/project.pbxproj @@ -41,6 +41,18 @@ 79641C212DEF1ED7004C970D /* RatingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C202DEF1ED7004C970D /* RatingList.swift */; }; 79641C232DEF2081004C970D /* RatingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C222DEF2081004C970D /* RatingViewModel.swift */; }; 79641C282DEF293A004C970D /* UserCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C272DEF293A004C970D /* UserCardView.swift */; }; + 796CB7962DF4269F0028631B /* CollectionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7952DF4269F0028631B /* CollectionRow.swift */; }; + 796CB7982DF42A620028631B /* UserNFTCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7972DF42A620028631B /* UserNFTCollection.swift */; }; + 796CB79A2DF42DB80028631B /* UserCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7992DF42DB80028631B /* UserCollectionViewModel.swift */; }; + 796CB79C2DF43B000028631B /* NftInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB79B2DF43B000028631B /* NftInfo.swift */; }; + 796CB79E2DF43B770028631B /* NftInfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB79D2DF43B770028631B /* NftInfoService.swift */; }; + 796CB7A02DF43C4E0028631B /* NftInfoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB79F2DF43C4E0028631B /* NftInfoRequest.swift */; }; + 796CB7A22DF4658D0028631B /* LikesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7A12DF4658D0028631B /* LikesService.swift */; }; + 796CB7A42DF467060028631B /* LikesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7A32DF467060028631B /* LikesRequest.swift */; }; + 796CB7A62DF46D500028631B /* UserLikes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7A52DF46D500028631B /* UserLikes.swift */; }; + 796CB7A82DF474330028631B /* UserOrdersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7A72DF474330028631B /* UserOrdersService.swift */; }; + 796CB7AA2DF474A30028631B /* UserOrders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7A92DF474A30028631B /* UserOrders.swift */; }; + 796CB7AC2DF474F70028631B /* UserOrdersRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CB7AB2DF474F70028631B /* UserOrdersRequest.swift */; }; 797AAE882DEF30E400745B0D /* UsersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797AAE872DEF30E400745B0D /* UsersService.swift */; }; 797AAE8A2DEF316100745B0D /* UsersRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797AAE892DEF316100745B0D /* UsersRequest.swift */; }; 79D0C7532DE396AC00D53241 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D0C7522DE396AC00D53241 /* Tab.swift */; }; @@ -108,6 +120,18 @@ 79641C202DEF1ED7004C970D /* RatingList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingList.swift; sourceTree = ""; }; 79641C222DEF2081004C970D /* RatingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RatingViewModel.swift; path = FakeNFT/RatingViewModel.swift; sourceTree = SOURCE_ROOT; }; 79641C272DEF293A004C970D /* UserCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCardView.swift; sourceTree = ""; }; + 796CB7952DF4269F0028631B /* CollectionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionRow.swift; sourceTree = ""; }; + 796CB7972DF42A620028631B /* UserNFTCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNFTCollection.swift; sourceTree = ""; }; + 796CB7992DF42DB80028631B /* UserCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserCollectionViewModel.swift; path = FakeNFT/UserCollectionViewModel.swift; sourceTree = SOURCE_ROOT; }; + 796CB79B2DF43B000028631B /* NftInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftInfo.swift; sourceTree = ""; }; + 796CB79D2DF43B770028631B /* NftInfoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftInfoService.swift; sourceTree = ""; }; + 796CB79F2DF43C4E0028631B /* NftInfoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftInfoRequest.swift; sourceTree = ""; }; + 796CB7A12DF4658D0028631B /* LikesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikesService.swift; sourceTree = ""; }; + 796CB7A32DF467060028631B /* LikesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikesRequest.swift; sourceTree = ""; }; + 796CB7A52DF46D500028631B /* UserLikes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikes.swift; sourceTree = ""; }; + 796CB7A72DF474330028631B /* UserOrdersService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOrdersService.swift; sourceTree = ""; }; + 796CB7A92DF474A30028631B /* UserOrders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOrders.swift; sourceTree = ""; }; + 796CB7AB2DF474F70028631B /* UserOrdersRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOrdersRequest.swift; sourceTree = ""; }; 797AAE872DEF30E400745B0D /* UsersService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersService.swift; sourceTree = ""; }; 797AAE892DEF316100745B0D /* UsersRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersRequest.swift; sourceTree = ""; }; 79D0C7522DE396AC00D53241 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; @@ -244,6 +268,9 @@ 0C79EE6B2A76DE2E00EE90EA /* ServicesAssemly.swift */, 558E39E62C68CE0900FB86AC /* NftService.swift */, 797AAE872DEF30E400745B0D /* UsersService.swift */, + 796CB79D2DF43B770028631B /* NftInfoService.swift */, + 796CB7A12DF4658D0028631B /* LikesService.swift */, + 796CB7A72DF474330028631B /* UserOrdersService.swift */, 0CFCB73F2A78002A0009A829 /* ExamplePutService.swift */, ); path = Services; @@ -295,6 +322,9 @@ children = ( 0CFCB7412A78013E0009A829 /* Nft.swift */, 79641C172DEF1D33004C970D /* User.swift */, + 796CB79B2DF43B000028631B /* NftInfo.swift */, + 796CB7A52DF46D500028631B /* UserLikes.swift */, + 796CB7A92DF474A30028631B /* UserOrders.swift */, ); path = Network; sourceTree = ""; @@ -306,6 +336,9 @@ 3FC8C39029D2453B0081F015 /* ExamplePutRequest.swift */, 0C79EE602A76DCD600EE90EA /* NftByIdRequest.swift */, 797AAE892DEF316100745B0D /* UsersRequest.swift */, + 796CB79F2DF43C4E0028631B /* NftInfoRequest.swift */, + 796CB7A32DF467060028631B /* LikesRequest.swift */, + 796CB7AB2DF474F70028631B /* UserOrdersRequest.swift */, ); path = Requests; sourceTree = ""; @@ -354,6 +387,7 @@ isa = PBXGroup; children = ( 79641C1B2DEF1E17004C970D /* RatingRow.swift */, + 796CB7952DF4269F0028631B /* CollectionRow.swift */, ); path = Row; sourceTree = ""; @@ -362,6 +396,7 @@ isa = PBXGroup; children = ( 79641C202DEF1ED7004C970D /* RatingList.swift */, + 796CB7972DF42A620028631B /* UserNFTCollection.swift */, ); path = List; sourceTree = ""; @@ -388,6 +423,7 @@ children = ( 79641C222DEF2081004C970D /* RatingViewModel.swift */, 79D9E0432DF03392005D5DB1 /* UserCardViewModel.swift */, + 796CB7992DF42DB80028631B /* UserCollectionViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -591,35 +627,47 @@ buildActionMask = 2147483647; files = ( 79641C182DEF1D33004C970D /* User.swift in Sources */, + 796CB7A62DF46D500028631B /* UserLikes.swift in Sources */, 3F478ECF29DB474E00F6D39E /* Colors.swift in Sources */, 3F478ED129DB476500F6D39E /* Fonts.swift in Sources */, 793BCC992DEF1B8800DF1252 /* StatisticsConstants.swift in Sources */, 3FC8C39329D246BA0081F015 /* DateFormatters+Presets.swift in Sources */, 79641C1E2DEF1E63004C970D /* UserAvatar.swift in Sources */, + 796CB79E2DF43B770028631B /* NftInfoService.swift in Sources */, 793BCC972DEF12F500DF1252 /* RatingView.swift in Sources */, + 796CB7A02DF43C4E0028631B /* NftInfoRequest.swift in Sources */, 0CFCB7422A78013E0009A829 /* Nft.swift in Sources */, + 796CB7962DF4269F0028631B /* CollectionRow.swift in Sources */, 79D0C7592DE39C3800D53241 /* NavigationBarStyle.swift in Sources */, 79D0C75D2DE39F5900D53241 /* LoadingView.swift in Sources */, 797AAE8A2DEF316100745B0D /* UsersRequest.swift in Sources */, 0CF2C2DD2A783CE600FDC837 /* ErrorView.swift in Sources */, 79D9E0402DF0316F005D5DB1 /* ShowWebViewButton.swift in Sources */, 79641C212DEF1ED7004C970D /* RatingList.swift in Sources */, + 796CB79A2DF42DB80028631B /* UserCollectionViewModel.swift in Sources */, + 796CB7AC2DF474F70028631B /* UserOrdersRequest.swift in Sources */, 5019A4CE2DE1E899009A5AF8 /* FakeNftApp.swift in Sources */, 79D9E0442DF03392005D5DB1 /* UserCardViewModel.swift in Sources */, 79641C1C2DEF1E17004C970D /* RatingRow.swift in Sources */, 79641C282DEF293A004C970D /* UserCardView.swift in Sources */, 0CFCB7402A78002A0009A829 /* ExamplePutService.swift in Sources */, 797AAE882DEF30E400745B0D /* UsersService.swift in Sources */, + 796CB7AA2DF474A30028631B /* UserOrders.swift in Sources */, 3F6806D529CBBEC700B4F915 /* NetworkTask.swift in Sources */, 558E39E72C68CE0A00FB86AC /* NftService.swift in Sources */, + 796CB7A42DF467060028631B /* LikesRequest.swift in Sources */, 0C79EE6C2A76DE2E00EE90EA /* ServicesAssemly.swift in Sources */, 793BCC902DEEF06800DF1252 /* WebViewRepresentable.swift in Sources */, + 796CB7A82DF474330028631B /* UserOrdersService.swift in Sources */, 79D0C7532DE396AC00D53241 /* Tab.swift in Sources */, 0CFCB74E2A7817DC0009A829 /* NftStorage.swift in Sources */, 3FC8C39129D2453B0081F015 /* ExamplePutRequest.swift in Sources */, 5019A4D02DE1E8E0009A5AF8 /* ContentView.swift in Sources */, + 796CB79C2DF43B000028631B /* NftInfo.swift in Sources */, 79D9E0422DF03260005D5DB1 /* ShowCollectionButton.swift in Sources */, + 796CB7A22DF4658D0028631B /* LikesService.swift in Sources */, 79641C232DEF2081004C970D /* RatingViewModel.swift in Sources */, + 796CB7982DF42A620028631B /* UserNFTCollection.swift in Sources */, 0C79EE612A76DCD600EE90EA /* NftByIdRequest.swift in Sources */, 79D9E0472DF03E41005D5DB1 /* UserCollectionView.swift in Sources */, 3F6806D329CBBE9600B4F915 /* NetworkRequest.swift in Sources */, diff --git a/FakeNFT/Docs/Statistics.md b/FakeNFT/Docs/Statistics.md index 19fe607bba..1210404aae 100644 --- a/FakeNFT/Docs/Statistics.md +++ b/FakeNFT/Docs/Statistics.md @@ -41,15 +41,15 @@ ## Module 3: #### Верстка -- Экран коллекции пользователя (est: 20 минут; fact: x часов). -- Ячейка с информацией об NFT (est: 1 час; fact: x часов). +- Экран коллекции пользователя (est: 20 минут; fact: 20 минут). +- Ячейка с информацией об NFT (est: 1 час; fact: 50 минут). #### Логика -- Обработка нажатия на сердечко (est: 1 час; fact: x часов). -- Обработка нажатия на кнопку корзины (добавить/удалить NFT) (est: 1 час; fact: x часов). +- Обработка нажатия на сердечко (est: 1 час; fact: 1 час). +- Обработка нажатия на кнопку корзины (добавить/удалить NFT) (est: 1 час; fact: 1 час). #### Работа с сетью -- Загрузка и отображение изображений NFT (est: 1 час; fact: x часов). -- Запрос на добавление/удаление лайка (est: 1 час; fact: x часов). -- Запрос на добавление NFT в корзину (est: 1 час; fact: x часов). -- Запрос на удаление NFT из корзины (est: 1 час; fact: x часов). +- Загрузка и отображение изображений NFT (est: 1 час; fact: 30 минут). +- Запрос на добавление/удаление лайка (est: 1 час; fact: 2 часа). +- Запрос на добавление NFT в корзину (est: 1 час; fact: 30 минут). +- Запрос на удаление NFT из корзины (est: 1 час; fact: 30 минут). diff --git a/FakeNFT/Models/Network/NftInfo.swift b/FakeNFT/Models/Network/NftInfo.swift new file mode 100644 index 0000000000..69ec6d5390 --- /dev/null +++ b/FakeNFT/Models/Network/NftInfo.swift @@ -0,0 +1,16 @@ +// +// NftInfo.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct NftInfo: Decodable, Hashable { + let id: String + let name: String + let images: [String] + let rating: Int + let price: Double +} diff --git a/FakeNFT/Models/Network/UserLikes.swift b/FakeNFT/Models/Network/UserLikes.swift new file mode 100644 index 0000000000..4fcea953fe --- /dev/null +++ b/FakeNFT/Models/Network/UserLikes.swift @@ -0,0 +1,12 @@ +// +// UserLikes.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct UserLikes: Decodable { + let likes: [String] +} diff --git a/FakeNFT/Models/Network/UserOrders.swift b/FakeNFT/Models/Network/UserOrders.swift new file mode 100644 index 0000000000..b9ec939d5f --- /dev/null +++ b/FakeNFT/Models/Network/UserOrders.swift @@ -0,0 +1,12 @@ +// +// UserOrders.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct UserOrders: Decodable { + let nfts: [String] +} diff --git a/FakeNFT/Scenes /Statistics/Common/List/UserNFTCollection.swift b/FakeNFT/Scenes /Statistics/Common/List/UserNFTCollection.swift new file mode 100644 index 0000000000..0804c2c424 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/List/UserNFTCollection.swift @@ -0,0 +1,57 @@ +// +// NFTCollection.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import SwiftUI + +struct UserNFTCollection: View { + + let nftInfo: [NftInfo] + let userLikes: UserLikes + let userOrders: UserOrders + var likeTapHandler: (NftInfo) -> Void + var cartTapHandler: (NftInfo) -> Void + + let columns = [ + GridItem(.flexible(), alignment: .top), + GridItem(.flexible(), alignment: .top), + GridItem(.flexible(), alignment: .top) + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: columns) { + ForEach(nftInfo, id: \.self) { nft in + CollectionRow( + nft: nft, + userLikes: userLikes, + userOrders: userOrders, + likeTapHandler: likeTapHandler, + cartTapHandler: cartTapHandler + ) + } + } + .padding(.horizontal) + } + } +} + +#Preview { + UserNFTCollection( + nftInfo: [ + NftInfo( + id: "", + name: "", + images: [""], + rating: 1, + price: 3.98 + )], + userLikes: UserLikes(likes: []), + userOrders: UserOrders(nfts: []), + likeTapHandler: {_ in }, + cartTapHandler: {_ in } + ) +} diff --git a/FakeNFT/Scenes /Statistics/Common/Row/CollectionRow.swift b/FakeNFT/Scenes /Statistics/Common/Row/CollectionRow.swift new file mode 100644 index 0000000000..97169eada9 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/Row/CollectionRow.swift @@ -0,0 +1,118 @@ +// +// CollectionRow.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import SwiftUI + +struct CollectionRow: View { + + // MARK: - Properties + + let nft: NftInfo + let userLikes: UserLikes + let userOrders: UserOrders + var likeTapHandler: (NftInfo) -> Void + var cartTapHandler: (NftInfo) -> Void + + // MARK: - Content + + var body: some View { + content + } + + // MARK: - View + + private var content: some View { + VStack(spacing: .zero) { + image + ratingView + .padding(.top, StatisticsConstants.anchorSmall) + nftInfo + .padding(.top, StatisticsConstants.rowAnchorSmall) + .padding(.bottom, StatisticsConstants.rowAnchorMedium) + } + .frame(width: StatisticsConstants.collectionRowSize) + } + + private var image: some View { + ZStack(alignment: .top) { + AsyncImage(url: URL(string: nft.images.first ?? "")) { phase in + switch phase { + case .success(let image): + image + .resizable() + .frame( + width: StatisticsConstants.collectionRowSize, + height: StatisticsConstants.collectionRowSize + ) + .aspectRatio(contentMode: .fill) + .clipShape(.rect(cornerRadius: StatisticsConstants.cornerRadiusSmall)) + case .failure, .empty: + RoundedRectangle(cornerRadius: StatisticsConstants.cornerRadiusSmall) + .fill(Color.lightGrayDay) + .frame( + width: StatisticsConstants.collectionRowSize, + height: StatisticsConstants.collectionRowSize + ) + default: + EmptyView() + } + } + HStack { + Spacer() + Button { + likeTapHandler(nft) + } label: { + Image(userLikes.likes.contains(nft.id) ? "likeActive" : "likeNoActive") + } + } + } + } + + private var ratingView: some View { + HStack(spacing: 2) { + let rating = nft.rating + ForEach(1..<6) { index in + Image(index <= rating ? "starActive" : "starNoActive") + } + Spacer() + } + } + + private var nftInfo: some View { + HStack(spacing: .zero) { + VStack(alignment: .leading) { + Text(nft.name) + .font(.bold17) + Text("\(nft.price, specifier: "%.2f") ETH") + .font(.medium10) + } + .foregroundStyle(Color.blackDay) + Spacer() + Button { + cartTapHandler(nft) + } label: { + Image(userOrders.nfts.contains(nft.id) ? "cartDelete" : "cartAdd") + } + } + } +} + +#Preview { + CollectionRow( + nft: NftInfo( + id: "", + name: "", + images: [""], + rating: 4, + price: 1.79 + ), + userLikes: UserLikes(likes: []), + userOrders: UserOrders(nfts: []), + likeTapHandler: {_ in }, + cartTapHandler: {_ in} + ) +} diff --git a/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift b/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift index 3bf5c6b50b..840fd8ca41 100644 --- a/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift +++ b/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift @@ -13,6 +13,7 @@ struct UserCardView: View { @Binding var isTabBarHidden: Bool @ObservedObject var viewModel: UserCardViewModel + @EnvironmentObject var service: ServicesAssembly // MARK: - Initializers @@ -48,7 +49,9 @@ struct UserCardView: View { .padding(.top, StatisticsConstants.topAnchorSmall) ShowWebViewButton(url: viewModel.user.website) .padding(.top, StatisticsConstants.topAnchorMedium) - NavigationLink(destination: UserCollectionView()) { + NavigationLink( + destination: UserCollectionView(nftIds: viewModel.user.nfts, service: service) + ) { ShowCollectionButton(nftsCount: viewModel.user.nfts.count) .padding(.top, StatisticsConstants.topAnchorLarge) } diff --git a/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift b/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift index 2abf89a8aa..6ae97da891 100644 --- a/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift +++ b/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift @@ -9,24 +9,73 @@ import SwiftUI struct UserCollectionView: View { - // TODO: 3/3 Statistics + // MARK: - Properties + + @ObservedObject var viewModel: UserCollectionViewModel + + // MARK: - Initializers + + init(nftIds: [String], service: ServicesAssembly) { + self.viewModel = UserCollectionViewModel(nftIds: nftIds, service: service) + } + + // MARK: - Content var body: some View { - VStack { - Text("Hello, World!") - } + content .modifier(NavigationBarStyle( title: "Коллекция NFT", backButtonHidden: false, filterButtonHidden: true, - filterButtonTapHandler: { } + filterButtonTapHandler: {} )) + .onAppear { + viewModel.loadData() + } + .alert(isPresented: $viewModel.isShowingErrorAlert) { + Alert( + title: Text("Не удалось получить данные"), + primaryButton: .default( + Text("Отмена") + ), + secondaryButton: .default( + Text("Повторить"), + action: { + viewModel.loadData() + } + ) + ) + } .toolbar(.hidden, for: .tabBar) } + + // MARK: - View + + private var content: some View { + ZStack { + UserNFTCollection( + nftInfo: viewModel.nftInfo, + userLikes: viewModel.userLikes, + userOrders: viewModel.userOrders, + likeTapHandler: { nft in + viewModel.updateLike(nftId: nft.id) + }, + cartTapHandler: { nft in + viewModel.updateUserOrder(nftId: nft.id) + } + ) + .padding(.top, StatisticsConstants.topAnchorSmall) + Text("Пусто") + .font(.bold17) + .opacity(viewModel.nftIds.isEmpty ? 1 : 0) + LoadingView() + .opacity(viewModel.isLoading ? 1 : 0) + } + } } #Preview { NavigationStack { - UserCollectionView() + UserCollectionView(nftIds: [""], service: ServicesAssembly()) } } diff --git a/FakeNFT/Services/LikesService.swift b/FakeNFT/Services/LikesService.swift new file mode 100644 index 0000000000..1e4228bf2f --- /dev/null +++ b/FakeNFT/Services/LikesService.swift @@ -0,0 +1,45 @@ +// +// LikesService.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +typealias LikesCompletion = (Result) -> Void +typealias LikesPutCompletion = (Result) -> Void + +protocol LikesService { + func loadLikes(completion: @escaping LikesCompletion) + func updateLikes(likes: UserLikes, completion: @escaping LikesPutCompletion) +} + +final class LikesServiceImpl: LikesService { + + private let networkClient: NetworkClient + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func loadLikes(completion: @escaping LikesCompletion) { + let request = LikesRequest() + networkClient.send(request: request, type: UserLikes.self) { result in + completion(result) + } + } + + func updateLikes(likes: UserLikes, completion: @escaping LikesPutCompletion) { + let dto = LikesDtoObject(likes: likes.likes) + let request = LikesPutRequest(dto: dto) + networkClient.send(request: request) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } +} diff --git a/FakeNFT/Services/NftInfoService.swift b/FakeNFT/Services/NftInfoService.swift new file mode 100644 index 0000000000..0e3f873ebd --- /dev/null +++ b/FakeNFT/Services/NftInfoService.swift @@ -0,0 +1,30 @@ +// +// NftInfoService.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +typealias NftInfoCompletion = (Result) -> Void + +protocol NftInfoService { + func loadNftInfo(id: String, completion: @escaping NftInfoCompletion) +} + +final class NftInfoServiceImpl: NftInfoService { + + private let networkClient: NetworkClient + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func loadNftInfo(id: String, completion: @escaping NftInfoCompletion) { + let request = NftInfoRequest(id: id) + networkClient.send(request: request, type: NftInfo.self) { result in + completion(result) + } + } +} diff --git a/FakeNFT/Services/Requests/LikesRequest.swift b/FakeNFT/Services/Requests/LikesRequest.swift new file mode 100644 index 0000000000..4f0affe24c --- /dev/null +++ b/FakeNFT/Services/Requests/LikesRequest.swift @@ -0,0 +1,31 @@ +// +// LikesRequest.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct LikesRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/profile/1") + } + var dto: Dto? +} + +struct LikesPutRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/profile/1") + } + var httpMethod: HttpMethod = .put + var dto: Dto? +} + +struct LikesDtoObject: Dto { + let likes: [String] + + func asDictionary() -> [String : String] { + ["likes" : likes.joined(separator: ",")] + } +} diff --git a/FakeNFT/Services/Requests/NftInfoRequest.swift b/FakeNFT/Services/Requests/NftInfoRequest.swift new file mode 100644 index 0000000000..4071071d89 --- /dev/null +++ b/FakeNFT/Services/Requests/NftInfoRequest.swift @@ -0,0 +1,16 @@ +// +// NftInfoRequest.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct NftInfoRequest: NetworkRequest { + let id: String + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/nft/\(id)") + } + var dto: Dto? +} diff --git a/FakeNFT/Services/Requests/UserOrdersRequest.swift b/FakeNFT/Services/Requests/UserOrdersRequest.swift new file mode 100644 index 0000000000..0b682a321f --- /dev/null +++ b/FakeNFT/Services/Requests/UserOrdersRequest.swift @@ -0,0 +1,31 @@ +// +// UserOrdersRequest.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +struct UserOrdersRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/orders/1") + } + var dto: Dto? +} + +struct UserOrdersPutRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/orders/1") + } + var httpMethod: HttpMethod = .put + var dto: Dto? +} + +struct UserOrdersDtoObject: Dto { + let nfts: [String] + + func asDictionary() -> [String : String] { + ["nfts" : nfts.joined(separator: ",")] + } +} diff --git a/FakeNFT/Services/ServicesAssemly.swift b/FakeNFT/Services/ServicesAssemly.swift index ae48e606c8..511a2ed148 100644 --- a/FakeNFT/Services/ServicesAssemly.swift +++ b/FakeNFT/Services/ServicesAssemly.swift @@ -23,4 +23,16 @@ final class ServicesAssembly: ObservableObject { var usersService: UsersService { UsersServiceImpl(networkClient: networkClient) } + + var nftInfoService: NftInfoService { + NftInfoServiceImpl(networkClient: networkClient) + } + + var likesService: LikesService { + LikesServiceImpl(networkClient: networkClient) + } + + var userOrdersService: UserOrdersService { + UserOrdersImpl(networkClient: networkClient) + } } diff --git a/FakeNFT/Services/UserOrdersService.swift b/FakeNFT/Services/UserOrdersService.swift new file mode 100644 index 0000000000..f59007b418 --- /dev/null +++ b/FakeNFT/Services/UserOrdersService.swift @@ -0,0 +1,45 @@ +// +// UserOrdersService.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +typealias UserOrdersCompletion = (Result) -> Void +typealias UserOrdersPutCompletion = (Result) -> Void + +protocol UserOrdersService { + func loadUserOrders(completion: @escaping UserOrdersCompletion) + func updateUserOrders(nfts: UserOrders, completion: @escaping UserOrdersPutCompletion) +} + +final class UserOrdersImpl: UserOrdersService { + + private let networkClient: NetworkClient + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func loadUserOrders(completion: @escaping UserOrdersCompletion) { + let request = UserOrdersRequest() + networkClient.send(request: request, type: UserOrders.self) { result in + completion(result) + } + } + + func updateUserOrders(nfts: UserOrders, completion: @escaping UserOrdersPutCompletion) { + let dto = UserOrdersDtoObject(nfts: nfts.nfts) + let request = UserOrdersPutRequest(dto: dto) + networkClient.send(request: request) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } +} diff --git a/FakeNFT/StatisticsConstants.swift b/FakeNFT/StatisticsConstants.swift index c29d1e4c41..89d92feb02 100644 --- a/FakeNFT/StatisticsConstants.swift +++ b/FakeNFT/StatisticsConstants.swift @@ -23,4 +23,8 @@ struct StatisticsConstants { static let buttonHeightMedium: CGFloat = 40.0 static let buttonHeightLarge: CGFloat = 54.0 static let borderLineWidth: CGFloat = 1.0 + + static let collectionRowSize: CGFloat = 108.0 + static let rowAnchorSmall: CGFloat = 5.0 + static let rowAnchorMedium: CGFloat = 20.0 } diff --git a/FakeNFT/UserCollectionViewModel.swift b/FakeNFT/UserCollectionViewModel.swift new file mode 100644 index 0000000000..ab4cfe3356 --- /dev/null +++ b/FakeNFT/UserCollectionViewModel.swift @@ -0,0 +1,145 @@ +// +// UserCollectionViewModel.swift +// FakeNFT +// +// Created by Anastasia on 07.06.2025. +// + +import Foundation + +@MainActor +final class UserCollectionViewModel: ObservableObject { + + // MARK: - Properties + + @Published var nftIds: [String] + @Published var nftInfo: [NftInfo] = [] + @Published var userLikes = UserLikes(likes: []) + @Published var userOrders = UserOrders(nfts: []) + @Published var isLoading: Bool = false + @Published var isShowingErrorAlert: Bool = false + + private let nftInfoService: NftInfoService + private let likesService: LikesService + private let userOrdersService: UserOrdersService + + // MARK: - Initializers + + init(nftIds: [String], service: ServicesAssembly) { + self.nftIds = nftIds + self.nftInfoService = service.nftInfoService + self.likesService = service.likesService + self.userOrdersService = service.userOrdersService + } + + // MARK: - Public Methods + + func loadData() { + if nftIds.isEmpty { return } + if !nftInfo.isEmpty { return } + isLoading = true + + let group = DispatchGroup() + + loadNftInfo(group) + loadLikes(group) + loadUserOrders(group) + + group.notify(queue: .main) { + self.isLoading = false + } + } + + func updateLike(nftId: String) { + userLikes = UserLikes( + likes: updateData( + array: userLikes.likes, + nftId: nftId + )) + + likesService.updateLikes(likes: userLikes) { result in + switch result { + case .success(): + print("Likes updated") + case .failure(let error): + print("Error update: \(error)") + } + } + } + + func updateUserOrder(nftId: String) { + userOrders = UserOrders( + nfts: updateData( + array: userOrders.nfts, + nftId: nftId + )) + + userOrdersService.updateUserOrders(nfts: userOrders) { result in + switch result { + case .success(): + print("Orders updated") + case .failure(let error): + print("Error update: \(error)") + } + } + } + + // MARK: - Private Methods + + private func loadNftInfo(_ group: DispatchGroup) { + for id in nftIds { + group.enter() + nftInfoService.loadNftInfo(id: id) { [weak self] result in + guard let self else { return } + switch result { + case .success(let nftInfo): + self.nftInfo.append(nftInfo) + case .failure(let error): + print("Error loading nft info: \(error)") + self.isShowingErrorAlert = true + } + group.leave() + } + } + } + + private func loadLikes(_ group: DispatchGroup) { + group.enter() + likesService.loadLikes() { [weak self] result in + guard let self else { return } + switch result { + case .success(let likes): + self.userLikes = likes + case .failure(let error): + print("Error loading likes: \(error)") + self.isShowingErrorAlert = true + } + group.leave() + } + } + + private func loadUserOrders(_ group: DispatchGroup) { + group.enter() + userOrdersService.loadUserOrders() { [weak self] result in + guard let self else { return } + switch result { + case .success(let orders): + self.userOrders = orders + case .failure(let error): + print("Error loading user orders: \(error)") + self.isShowingErrorAlert = true + } + group.leave() + } + } + + private func updateData(array: [T], nftId: T) -> [T] { + var updatedArray = array + if array.contains(nftId) { + updatedArray.removeAll { $0 == nftId } + } else { + updatedArray.append(nftId) + } + return updatedArray + } +}