diff --git a/FakeNFT.xcodeproj/project.pbxproj b/FakeNFT.xcodeproj/project.pbxproj index 0363aa941e..de49900131 100644 --- a/FakeNFT.xcodeproj/project.pbxproj +++ b/FakeNFT.xcodeproj/project.pbxproj @@ -33,10 +33,36 @@ 793BCC902DEEF06800DF1252 /* WebViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793BCC8F2DEEF06800DF1252 /* WebViewRepresentable.swift */; }; 793BCC922DEEF46600DF1252 /* Statistics.md in Resources */ = {isa = PBXBuildFile; fileRef = 793BCC912DEEF46600DF1252 /* Statistics.md */; }; 793BCC942DEEF47200DF1252 /* CartAnisimov.md in Resources */ = {isa = PBXBuildFile; fileRef = 793BCC932DEEF47200DF1252 /* CartAnisimov.md */; }; + 793BCC972DEF12F500DF1252 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793BCC962DEF12F500DF1252 /* RatingView.swift */; }; + 793BCC992DEF1B8800DF1252 /* StatisticsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793BCC982DEF1B8800DF1252 /* StatisticsConstants.swift */; }; + 79641C182DEF1D33004C970D /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C172DEF1D33004C970D /* User.swift */; }; + 79641C1C2DEF1E17004C970D /* RatingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C1B2DEF1E17004C970D /* RatingRow.swift */; }; + 79641C1E2DEF1E63004C970D /* UserAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79641C1D2DEF1E63004C970D /* UserAvatar.swift */; }; + 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 */; }; 79D0C7562DE3973200D53241 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D0C7552DE3973200D53241 /* TabBarView.swift */; }; 79D0C7592DE39C3800D53241 /* NavigationBarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D0C7582DE39C3800D53241 /* NavigationBarStyle.swift */; }; 79D0C75D2DE39F5900D53241 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D0C75C2DE39F5900D53241 /* LoadingView.swift */; }; + 79D9E0402DF0316F005D5DB1 /* ShowWebViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D9E03F2DF0316F005D5DB1 /* ShowWebViewButton.swift */; }; + 79D9E0422DF03260005D5DB1 /* ShowCollectionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D9E0412DF03260005D5DB1 /* ShowCollectionButton.swift */; }; + 79D9E0442DF03392005D5DB1 /* UserCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D9E0432DF03392005D5DB1 /* UserCardViewModel.swift */; }; + 79D9E0472DF03E41005D5DB1 /* UserCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D9E0462DF03E41005D5DB1 /* UserCollectionView.swift */; }; E1CD40DC2A96BECC00BE7FE8 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */; }; /* End PBXBuildFile section */ @@ -86,10 +112,36 @@ 793BCC8F2DEEF06800DF1252 /* WebViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewRepresentable.swift; sourceTree = ""; }; 793BCC912DEEF46600DF1252 /* Statistics.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Statistics.md; sourceTree = ""; }; 793BCC932DEEF47200DF1252 /* CartAnisimov.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CartAnisimov.md; sourceTree = ""; }; + 793BCC962DEF12F500DF1252 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = ""; }; + 793BCC982DEF1B8800DF1252 /* StatisticsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StatisticsConstants.swift; path = FakeNFT/StatisticsConstants.swift; sourceTree = SOURCE_ROOT; }; + 79641C172DEF1D33004C970D /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 79641C1B2DEF1E17004C970D /* RatingRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingRow.swift; sourceTree = ""; }; + 79641C1D2DEF1E63004C970D /* UserAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatar.swift; sourceTree = ""; }; + 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 = ""; }; 79D0C7552DE3973200D53241 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 79D0C7582DE39C3800D53241 /* NavigationBarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarStyle.swift; sourceTree = ""; }; 79D0C75C2DE39F5900D53241 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 79D9E03F2DF0316F005D5DB1 /* ShowWebViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowWebViewButton.swift; sourceTree = ""; }; + 79D9E0412DF03260005D5DB1 /* ShowCollectionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowCollectionButton.swift; sourceTree = ""; }; + 79D9E0432DF03392005D5DB1 /* UserCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserCardViewModel.swift; path = FakeNFT/UserCardViewModel.swift; sourceTree = SOURCE_ROOT; }; + 79D9E0462DF03E41005D5DB1 /* UserCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCollectionView.swift; sourceTree = ""; }; E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; /* End PBXFileReference section */ @@ -215,6 +267,10 @@ 3FC8C38F29D245250081F015 /* Requests */, 0C79EE6B2A76DE2E00EE90EA /* ServicesAssemly.swift */, 558E39E62C68CE0900FB86AC /* NftService.swift */, + 797AAE872DEF30E400745B0D /* UsersService.swift */, + 796CB79D2DF43B770028631B /* NftInfoService.swift */, + 796CB7A12DF4658D0028631B /* LikesService.swift */, + 796CB7A72DF474330028631B /* UserOrdersService.swift */, 0CFCB73F2A78002A0009A829 /* ExamplePutService.swift */, ); path = Services; @@ -224,6 +280,7 @@ isa = PBXGroup; children = ( 5019A4CF2DE1E8E0009A5AF8 /* ContentView.swift */, + 793BCC952DEF12D900DF1252 /* Statistics */, 0CF2C2D92A783C1600FDC837 /* Common */, 79D0C7572DE39C1100D53241 /* ViewModifier */, 79D0C7542DE396D400D53241 /* TabBar */, @@ -264,6 +321,10 @@ isa = PBXGroup; children = ( 0CFCB7412A78013E0009A829 /* Nft.swift */, + 79641C172DEF1D33004C970D /* User.swift */, + 796CB79B2DF43B000028631B /* NftInfo.swift */, + 796CB7A52DF46D500028631B /* UserLikes.swift */, + 796CB7A92DF474A30028631B /* UserOrders.swift */, ); path = Network; sourceTree = ""; @@ -274,6 +335,10 @@ 0C79EE622A76DD1900EE90EA /* RequestConstants.swift */, 3FC8C39029D2453B0081F015 /* ExamplePutRequest.swift */, 0C79EE602A76DCD600EE90EA /* NftByIdRequest.swift */, + 797AAE892DEF316100745B0D /* UsersRequest.swift */, + 796CB79F2DF43C4E0028631B /* NftInfoRequest.swift */, + 796CB7A32DF467060028631B /* LikesRequest.swift */, + 796CB7AB2DF474F70028631B /* UserOrdersRequest.swift */, ); path = Requests; sourceTree = ""; @@ -287,6 +352,63 @@ path = WebView; sourceTree = ""; }; + 793BCC952DEF12D900DF1252 /* Statistics */ = { + isa = PBXGroup; + children = ( + 793BCC9A2DEF1BC500DF1252 /* RatingView */, + 79641C262DEF292A004C970D /* UserCardView */, + 79D9E0452DF03E34005D5DB1 /* UserCollectionView */, + 79641C192DEF1D63004C970D /* Common */, + ); + path = Statistics; + sourceTree = ""; + }; + 793BCC9A2DEF1BC500DF1252 /* RatingView */ = { + isa = PBXGroup; + children = ( + 793BCC962DEF12F500DF1252 /* RatingView.swift */, + ); + path = RatingView; + sourceTree = ""; + }; + 79641C192DEF1D63004C970D /* Common */ = { + isa = PBXGroup; + children = ( + 79641C1F2DEF1ECB004C970D /* List */, + 79641C1A2DEF1DF4004C970D /* Row */, + 79641C1D2DEF1E63004C970D /* UserAvatar.swift */, + 79D9E03F2DF0316F005D5DB1 /* ShowWebViewButton.swift */, + 79D9E0412DF03260005D5DB1 /* ShowCollectionButton.swift */, + ); + path = Common; + sourceTree = ""; + }; + 79641C1A2DEF1DF4004C970D /* Row */ = { + isa = PBXGroup; + children = ( + 79641C1B2DEF1E17004C970D /* RatingRow.swift */, + 796CB7952DF4269F0028631B /* CollectionRow.swift */, + ); + path = Row; + sourceTree = ""; + }; + 79641C1F2DEF1ECB004C970D /* List */ = { + isa = PBXGroup; + children = ( + 79641C202DEF1ED7004C970D /* RatingList.swift */, + 796CB7972DF42A620028631B /* UserNFTCollection.swift */, + ); + path = List; + sourceTree = ""; + }; + 79641C262DEF292A004C970D /* UserCardView */ = { + isa = PBXGroup; + children = ( + 79641C272DEF293A004C970D /* UserCardView.swift */, + ); + path = UserCardView; + sourceTree = ""; + }; 79D0C74F2DE393F600D53241 /* Docs */ = { isa = PBXGroup; children = ( @@ -299,6 +421,9 @@ 79D0C7502DE3940800D53241 /* ViewModels */ = { isa = PBXGroup; children = ( + 79641C222DEF2081004C970D /* RatingViewModel.swift */, + 79D9E0432DF03392005D5DB1 /* UserCardViewModel.swift */, + 796CB7992DF42DB80028631B /* UserCollectionViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -330,6 +455,7 @@ 79D0C75A2DE39E1100D53241 /* Constants */ = { isa = PBXGroup; children = ( + 793BCC982DEF1B8800DF1252 /* StatisticsConstants.swift */, ); path = Constants; sourceTree = ""; @@ -342,6 +468,14 @@ path = LoadingView; sourceTree = ""; }; + 79D9E0452DF03E34005D5DB1 /* UserCollectionView */ = { + isa = PBXGroup; + children = ( + 79D9E0462DF03E41005D5DB1 /* UserCollectionView.swift */, + ); + path = UserCollectionView; + sourceTree = ""; + }; E1CD40DA2A96BE9B00BE7FE8 /* Resources */ = { isa = PBXGroup; children = ( @@ -492,24 +626,50 @@ isa = PBXSourcesBuildPhase; 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 */, 3F6806D129CBBE6B00B4F915 /* NetworkClient.swift in Sources */, 79D0C7562DE3973200D53241 /* TabBarView.swift in Sources */, diff --git a/FakeNFT/Docs/Statistics.md b/FakeNFT/Docs/Statistics.md index 34b26815ed..1210404aae 100644 --- a/FakeNFT/Docs/Statistics.md +++ b/FakeNFT/Docs/Statistics.md @@ -14,42 +14,42 @@ ## Модуль 1: #### Верстка -- Экран рейтинга (кнопка сортировки, список пользователей) (est: 30 минут; fact: x часов). -- Ячейка для списка пользователей на экране рейтинга (est: 1 час; fact: x часов). +- Экран рейтинга (кнопка сортировки, список пользователей) (est: 30 минут; fact: 30 минут). +- Ячейка для списка пользователей на экране рейтинга (est: 1 час; fact: 30 минут). #### Логика -- Сортировка списка пользователей (по имени, рейтингу) (est: 1 час; fact: x часов). -- Сохранение выбранного способа сортировки (est: 1 час; fact: x часов). -- Навигация на экран информации о пользователе (est: 20 минут; fact: x часов). +- Сортировка списка пользователей (по имени, рейтингу) (est: 1 час; fact: 20 минут). +- Сохранение выбранного способа сортировки (est: 1 час; fact: 25 минут). +- Навигация на экран информации о пользователе (est: 20 минут; fact: 15 минут). #### Работа с сетью -- Создать запрос на получение списка пользователей (est: 2 часа; fact: x часов). -- Подключить запрос (est: 1 час; fact: x часов). +- Создать запрос на получение списка пользователей (est: 2 часа; fact: 1 час). +- Подключить запрос (est: 1 час; fact: 30 минут). ## Модуль 2: #### Верстка -- Экран информации о пользователе (аватар, имя, описание) (est: 30 минут; fact: x часов). -- Кнопка перехода на сайт пользователя (est: 30 минут; fact: x часов). -- Кнопка перехода на экран коллекции пользователя (est: 30 минут; fact: x часов). -- Создать WebView (est: 1 час; fact: x часов). +- Экран информации о пользователе (аватар, имя, описание) (est: 30 минут; fact: 30 минут). +- Кнопка перехода на сайт пользователя (est: 30 минут; fact: 20 минут). +- Кнопка перехода на экран коллекции пользователя (est: 30 минут; fact: 30 минут). +- Создать WebView (est: 1 час; fact: 1 час). #### Логика -- Открытие сайта пользователя в WebView (est: 30 минут; fact: x часов). -- Навигация на экран коллекции пользователя (est: 20 минут; fact: x часов). +- Открытие сайта пользователя в WebView (est: 30 минут; fact: 10 минут). +- Навигация на экран коллекции пользователя (est: 20 минут; fact: 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/User.swift b/FakeNFT/Models/Network/User.swift new file mode 100644 index 0000000000..44e25e2e2a --- /dev/null +++ b/FakeNFT/Models/Network/User.swift @@ -0,0 +1,18 @@ +// +// User.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import Foundation + +struct User: Decodable, Identifiable { + let id: String + let name: String + let avatar: String + let description: String? + let website: String + let nfts: [String] + let rating: String +} 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/RatingViewModel.swift b/FakeNFT/RatingViewModel.swift new file mode 100644 index 0000000000..e47d2d7c44 --- /dev/null +++ b/FakeNFT/RatingViewModel.swift @@ -0,0 +1,59 @@ +// +// RatingViewModel.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +@MainActor +final class RatingViewModel: ObservableObject { + @Published var filteredUsers: [User] = [] + @Published var isLoading: Bool = false + @Published var isShowingFilterSheet: Bool = false + @Published var isShowingErrorAlert: Bool = false + private var users: [User] = [] + private let usersService: UsersService + + @AppStorage("ratingSort") private var currentSorting: RatingFilterType = .rating + + init(usersService: UsersService) { + self.usersService = usersService + } + + func loadUsers() { + if !users.isEmpty { return } + isLoading = true + + usersService.loadUsers { [weak self] result in + DispatchQueue.main.async { + guard let self else { return } + self.isLoading = false + switch result { + case .success(let users): + self.users = users + self.filterUsers(by: self.currentSorting) + case .failure(_): + self.isShowingErrorAlert = true + } + } + } + } + + func filterUsers(by type: RatingFilterType) { + switch type { + case .name: + currentSorting = .name + filteredUsers = users.sorted { $0.name.lowercased() < $1.name.lowercased() } + case .rating: + currentSorting = .rating + filteredUsers = users.sorted { Int($0.rating) ?? 0 < Int($1.rating) ?? 0 } + } + } +} + +enum RatingFilterType: String { + case name + case rating +} diff --git a/FakeNFT/Scenes /ContentView.swift b/FakeNFT/Scenes /ContentView.swift index 8c3235e23e..0c8d7f50f8 100644 --- a/FakeNFT/Scenes /ContentView.swift +++ b/FakeNFT/Scenes /ContentView.swift @@ -25,7 +25,10 @@ struct ContentView: View { Text("Cart") .tag(Tab.catalog) - Text("Statistics") + RatingView( + viewModel: RatingViewModel(usersService: service.usersService), + isTabBarHidden: $isTabBarHidden + ) .tag(Tab.statistics) } if !isTabBarHidden { diff --git a/FakeNFT/Scenes /Statistics/Common/List/RatingList.swift b/FakeNFT/Scenes /Statistics/Common/List/RatingList.swift new file mode 100644 index 0000000000..bca32dfa66 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/List/RatingList.swift @@ -0,0 +1,34 @@ +// +// RatingList.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct RatingList: View { + + // MARK: - Properties + + let isLoading: Bool + @ViewBuilder var content: Content + + // MARK: - Content + + var body: some View { + if isLoading { + LoadingView() + } else { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: StatisticsConstants.anchorSmall) { + content + } + } + } + } +} + +#Preview { + RatingList(isLoading: true) {} +} 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/Common/Row/RatingRow.swift b/FakeNFT/Scenes /Statistics/Common/Row/RatingRow.swift new file mode 100644 index 0000000000..85d512cde6 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/Row/RatingRow.swift @@ -0,0 +1,65 @@ +// +// RatingRow.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct RatingRow: View { + + // MARK: - Properties + + let user: User + + // MARK: - View + + var body: some View { + HStack(spacing: StatisticsConstants.anchorSmall) { + Text(user.rating) + .font(.regular15) + .foregroundStyle(Color.blackDay) + .frame(width: StatisticsConstants.ratingLabelWidth) + RoundedRectangle(cornerRadius: StatisticsConstants.cornerRadiusSmall) + .fill(Color.lightGrayDay) + .frame(height: StatisticsConstants.ratingRowHeight) + .overlay { + userInfo() + } + } + } + + private func userInfo() -> some View { + HStack(spacing: .zero) { + UserAvatar( + url: user.avatar, + size: StatisticsConstants.avatarSizeSmall + ) + .padding(.leading) + Group { + Text(user.name) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, StatisticsConstants.anchorSmall) + Text("\(user.nfts.count)") + .padding(.horizontal) + } + .font(.bold22) + .foregroundStyle(Color.blackDay) + } + } +} + +#Preview { + let user = User( + id: "1", + name: "Joaquin Phoenix", + avatar: "", + description: "", + website: "", + nfts: ["1", "2", "3"], + rating: "1" + ) + RatingRow(user: user) +} diff --git a/FakeNFT/Scenes /Statistics/Common/ShowCollectionButton.swift b/FakeNFT/Scenes /Statistics/Common/ShowCollectionButton.swift new file mode 100644 index 0000000000..c08a0be7d3 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/ShowCollectionButton.swift @@ -0,0 +1,31 @@ +// +// ShowCollectionButton.swift +// FakeNFT +// +// Created by Anastasia on 04.06.2025. +// + +import SwiftUI + +struct ShowCollectionButton: View { + + let nftsCount: Int + + var body: some View { + HStack { + Group { + Text("Коллекция NFT") + Text("(\(nftsCount))") + Spacer() + Image(systemName: "chevron.forward") + } + .font(.bold17) + .foregroundColor(Color.blackDay) + } + .frame(height: StatisticsConstants.buttonHeightLarge) + } +} + +#Preview { + ShowCollectionButton(nftsCount: 122) +} diff --git a/FakeNFT/Scenes /Statistics/Common/ShowWebViewButton.swift b/FakeNFT/Scenes /Statistics/Common/ShowWebViewButton.swift new file mode 100644 index 0000000000..3b5ec8a73c --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/ShowWebViewButton.swift @@ -0,0 +1,41 @@ +// +// ShowWebViewButton.swift +// FakeNFT +// +// Created by Anastasia on 04.06.2025. +// + +import SwiftUI + +struct ShowWebViewButton: View { + + let url: String + + var body: some View { + NavigationLink( + destination: WebView(url: URL(string: url)) + .toolbar(.hidden, for: .tabBar) + ) { + Text("Перейти на сайт пользователя") + .font(.regular15) + .foregroundStyle(Color.blackDay) + .frame( + maxWidth: .infinity, + maxHeight: StatisticsConstants.buttonHeightMedium + ) + .background( + RoundedRectangle(cornerRadius: StatisticsConstants.cornerRadiusMedium) + .stroke( + Color.blackDay, + lineWidth: StatisticsConstants.borderLineWidth + ) + ) + } + } +} + +#Preview { + NavigationStack { + ShowWebViewButton(url: "") + } +} diff --git a/FakeNFT/Scenes /Statistics/Common/UserAvatar.swift b/FakeNFT/Scenes /Statistics/Common/UserAvatar.swift new file mode 100644 index 0000000000..960ec5638c --- /dev/null +++ b/FakeNFT/Scenes /Statistics/Common/UserAvatar.swift @@ -0,0 +1,37 @@ +// +// UserAvatar.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct UserAvatar: View { + + let url: String + let size: CGFloat + + var body: some View { + AsyncImage(url: URL(string: url)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .frame(width: size, height: size) + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + case .failure, .empty: + Image("userpick") + .resizable() + .frame(width: size, height: size) + default: + EmptyView() + } + } + } +} + +#Preview { + UserAvatar(url: "", size: 28.0) +} diff --git a/FakeNFT/Scenes /Statistics/RatingView/RatingView.swift b/FakeNFT/Scenes /Statistics/RatingView/RatingView.swift new file mode 100644 index 0000000000..f94d4b9264 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/RatingView/RatingView.swift @@ -0,0 +1,92 @@ +// +// RatingView.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct RatingView: View { + + // MARK: - Properties + + @StateObject var viewModel: RatingViewModel + @Binding var isTabBarHidden: Bool + + // MARK: - Content + + var body: some View { + NavigationStack { + content + .onAppear { + viewModel.loadUsers() + } + } + } + + // MARK: - View + + private var content: some View { + ratingList + .modifier(NavigationBarStyle( + title: nil, + backButtonHidden: true, + filterButtonHidden: false, + filterButtonTapHandler: { + viewModel.isShowingFilterSheet = true + } + )) + .confirmationDialog( + "Сортировка", + isPresented: $viewModel.isShowingFilterSheet, + titleVisibility: .visible + ) { + Button("По имени") { + viewModel.filterUsers(by: .name) + } + Button("По рейтингу") { + viewModel.filterUsers(by: .rating) + } + } + .alert(isPresented: $viewModel.isShowingErrorAlert) { + Alert( + title: Text("Не удалось получить данные"), + primaryButton: .default( + Text("Отмена") + ), + secondaryButton: .default( + Text("Повторить"), + action: { + viewModel.loadUsers() + } + ) + ) + } + } + + private var ratingList: some View { + VStack(spacing: .zero) { + RatingList(isLoading: viewModel.isLoading) { + ForEach(viewModel.filteredUsers) { user in + NavigationLink(destination: UserCardView( + user: user, + isTabBarHidden: $isTabBarHidden + )) { + RatingRow(user: user) + } + } + } + } + .padding(.horizontal) + .padding(.top, StatisticsConstants.topAnchorSmall) + } +} + +#Preview { + let services = ServicesAssembly() + RatingView( + viewModel: RatingViewModel(usersService: services.usersService), + isTabBarHidden: .constant(true) + ) +} diff --git a/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift b/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift new file mode 100644 index 0000000000..840fd8ca41 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift @@ -0,0 +1,94 @@ +// +// UserCardView.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct UserCardView: View { + + // MARK: - Properties + + @Binding var isTabBarHidden: Bool + @ObservedObject var viewModel: UserCardViewModel + @EnvironmentObject var service: ServicesAssembly + + // MARK: - Initializers + + init(user: User, isTabBarHidden: Binding) { + self.viewModel = UserCardViewModel(user: user) + self._isTabBarHidden = isTabBarHidden + } + + // MARK: - Content + + var body: some View { + content + .modifier(NavigationBarStyle( + title: nil, + backButtonHidden: false, + filterButtonHidden: true, + isTabBarHidden: $isTabBarHidden, + filterButtonTapHandler: { } + )) + .onAppear { + isTabBarHidden = true + } + } + + // MARK: - View + + private var content: some View { + VStack(alignment: .leading) { + userInfo + Text(viewModel.user.description ?? "") + .font(.footnote) + .foregroundStyle(Color.blackDay) + .padding(.top, StatisticsConstants.topAnchorSmall) + ShowWebViewButton(url: viewModel.user.website) + .padding(.top, StatisticsConstants.topAnchorMedium) + NavigationLink( + destination: UserCollectionView(nftIds: viewModel.user.nfts, service: service) + ) { + ShowCollectionButton(nftsCount: viewModel.user.nfts.count) + .padding(.top, StatisticsConstants.topAnchorLarge) + } + Spacer() + } + .padding(.top, StatisticsConstants.topAnchorSmall) + .padding(.horizontal) + } + + private var userInfo: some View { + HStack(spacing: .zero) { + UserAvatar( + url: viewModel.user.avatar, + size: StatisticsConstants.avatarSizeLarge + ) + Text(viewModel.user.name) + .font(.bold22) + .foregroundStyle(Color.blackDay) + .padding(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +#Preview { + let userTest = User(id: "1", + name: "Joaquin Phoenix", + avatar: "", + description: "Дизайнер из Казани, люблю цифровое искусство и бейглы. В моей коллекции уже 100+ NFT, и еще больше — на моём сайте. Открыт к коллаборациям.", + website: "", + nfts: ["1", "2", "3", "3", "3", "3"], + rating: "1" + ) + NavigationView { + UserCardView( + user: userTest, + isTabBarHidden: .constant(true) + ) + } +} diff --git a/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift b/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift new file mode 100644 index 0000000000..6ae97da891 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/UserCollectionView/UserCollectionView.swift @@ -0,0 +1,81 @@ +// +// UserCollectionView.swift +// FakeNFT +// +// Created by Anastasia on 04.06.2025. +// + +import SwiftUI + +struct UserCollectionView: View { + + // 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 { + content + .modifier(NavigationBarStyle( + title: "Коллекция NFT", + backButtonHidden: false, + filterButtonHidden: true, + 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(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/RequestConstants.swift b/FakeNFT/Services/Requests/RequestConstants.swift index 9d3529893b..abd529c431 100644 --- a/FakeNFT/Services/Requests/RequestConstants.swift +++ b/FakeNFT/Services/Requests/RequestConstants.swift @@ -1,5 +1,4 @@ enum RequestConstants { static let baseURL = "https://d5dn3j2ouj72b0ejucbl.apigw.yandexcloud.net" - #warning("Instert your token here") - static let token = "" + static let token = "aa87dab8-01e5-4aa8-957e-951e3edcb363" } 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/Requests/UsersRequest.swift b/FakeNFT/Services/Requests/UsersRequest.swift new file mode 100644 index 0000000000..d9e8bf878f --- /dev/null +++ b/FakeNFT/Services/Requests/UsersRequest.swift @@ -0,0 +1,15 @@ +// +// UsersRequest.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import Foundation + +struct UsersRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/users") + } + var dto: Dto? +} diff --git a/FakeNFT/Services/ServicesAssemly.swift b/FakeNFT/Services/ServicesAssemly.swift index 49b8dbd1c9..511a2ed148 100644 --- a/FakeNFT/Services/ServicesAssemly.swift +++ b/FakeNFT/Services/ServicesAssemly.swift @@ -19,4 +19,20 @@ final class ServicesAssembly: ObservableObject { storage: nftStorage ) } + + 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/Services/UsersService.swift b/FakeNFT/Services/UsersService.swift new file mode 100644 index 0000000000..af1b104957 --- /dev/null +++ b/FakeNFT/Services/UsersService.swift @@ -0,0 +1,30 @@ +// +// UsersService.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import Foundation + +typealias UsersCompletion = (Result<[User], Error>) -> Void + +protocol UsersService { + func loadUsers(completion: @escaping UsersCompletion) +} + +final class UsersServiceImpl: UsersService { + + private let networkClient: NetworkClient + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func loadUsers(completion: @escaping UsersCompletion) { + let request = UsersRequest() + networkClient.send(request: request, type: [User].self) { result in + completion(result) + } + } +} diff --git a/FakeNFT/StatisticsConstants.swift b/FakeNFT/StatisticsConstants.swift new file mode 100644 index 0000000000..89d92feb02 --- /dev/null +++ b/FakeNFT/StatisticsConstants.swift @@ -0,0 +1,30 @@ +// +// StatisticsConstants.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import Foundation + +struct StatisticsConstants { + static let anchorSmall: CGFloat = 8.0 + static let topAnchorSmall: CGFloat = 20.0 + static let topAnchorMedium: CGFloat = 28.0 + static let topAnchorLarge: CGFloat = 40.0 + + static let cornerRadiusSmall: CGFloat = 12.0 + static let cornerRadiusMedium: CGFloat = 16.0 + + static let ratingRowHeight: CGFloat = 80.0 + static let avatarSizeSmall: CGFloat = 28.0 + static let avatarSizeLarge: CGFloat = 70.0 + static let ratingLabelWidth: CGFloat = 27.0 + 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/UserCardViewModel.swift b/FakeNFT/UserCardViewModel.swift new file mode 100644 index 0000000000..78af3f0478 --- /dev/null +++ b/FakeNFT/UserCardViewModel.swift @@ -0,0 +1,17 @@ +// +// UserCardViewModel.swift +// FakeNFT +// +// Created by Anastasia on 04.06.2025. +// + +import Foundation + +@MainActor +final class UserCardViewModel: ObservableObject { + @Published var user: User + + init(user: User) { + self.user = user + } +} 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 + } +}