diff --git a/FakeNFT.xcodeproj/project.pbxproj b/FakeNFT.xcodeproj/project.pbxproj index 0363aa941e..160cc2dcb7 100644 --- a/FakeNFT.xcodeproj/project.pbxproj +++ b/FakeNFT.xcodeproj/project.pbxproj @@ -33,6 +33,16 @@ 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 */; }; + 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 */; }; @@ -86,6 +96,16 @@ 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 = ""; }; + 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 = ""; }; @@ -215,6 +235,7 @@ 3FC8C38F29D245250081F015 /* Requests */, 0C79EE6B2A76DE2E00EE90EA /* ServicesAssemly.swift */, 558E39E62C68CE0900FB86AC /* NftService.swift */, + 797AAE872DEF30E400745B0D /* UsersService.swift */, 0CFCB73F2A78002A0009A829 /* ExamplePutService.swift */, ); path = Services; @@ -224,6 +245,7 @@ isa = PBXGroup; children = ( 5019A4CF2DE1E8E0009A5AF8 /* ContentView.swift */, + 793BCC952DEF12D900DF1252 /* Statistics */, 0CF2C2D92A783C1600FDC837 /* Common */, 79D0C7572DE39C1100D53241 /* ViewModifier */, 79D0C7542DE396D400D53241 /* TabBar */, @@ -264,6 +286,7 @@ isa = PBXGroup; children = ( 0CFCB7412A78013E0009A829 /* Nft.swift */, + 79641C172DEF1D33004C970D /* User.swift */, ); path = Network; sourceTree = ""; @@ -274,6 +297,7 @@ 0C79EE622A76DD1900EE90EA /* RequestConstants.swift */, 3FC8C39029D2453B0081F015 /* ExamplePutRequest.swift */, 0C79EE602A76DCD600EE90EA /* NftByIdRequest.swift */, + 797AAE892DEF316100745B0D /* UsersRequest.swift */, ); path = Requests; sourceTree = ""; @@ -287,6 +311,58 @@ path = WebView; sourceTree = ""; }; + 793BCC952DEF12D900DF1252 /* Statistics */ = { + isa = PBXGroup; + children = ( + 793BCC9A2DEF1BC500DF1252 /* RatingView */, + 79641C262DEF292A004C970D /* UserCardView */, + 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 */, + ); + path = Common; + sourceTree = ""; + }; + 79641C1A2DEF1DF4004C970D /* Row */ = { + isa = PBXGroup; + children = ( + 79641C1B2DEF1E17004C970D /* RatingRow.swift */, + ); + path = Row; + sourceTree = ""; + }; + 79641C1F2DEF1ECB004C970D /* List */ = { + isa = PBXGroup; + children = ( + 79641C202DEF1ED7004C970D /* RatingList.swift */, + ); + path = List; + sourceTree = ""; + }; + 79641C262DEF292A004C970D /* UserCardView */ = { + isa = PBXGroup; + children = ( + 79641C272DEF293A004C970D /* UserCardView.swift */, + ); + path = UserCardView; + sourceTree = ""; + }; 79D0C74F2DE393F600D53241 /* Docs */ = { isa = PBXGroup; children = ( @@ -299,6 +375,7 @@ 79D0C7502DE3940800D53241 /* ViewModels */ = { isa = PBXGroup; children = ( + 79641C222DEF2081004C970D /* RatingViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -330,6 +407,7 @@ 79D0C75A2DE39E1100D53241 /* Constants */ = { isa = PBXGroup; children = ( + 793BCC982DEF1B8800DF1252 /* StatisticsConstants.swift */, ); path = Constants; sourceTree = ""; @@ -492,15 +570,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 79641C182DEF1D33004C970D /* User.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 */, + 793BCC972DEF12F500DF1252 /* RatingView.swift in Sources */, 0CFCB7422A78013E0009A829 /* Nft.swift in Sources */, 79D0C7592DE39C3800D53241 /* NavigationBarStyle.swift in Sources */, 79D0C75D2DE39F5900D53241 /* LoadingView.swift in Sources */, + 797AAE8A2DEF316100745B0D /* UsersRequest.swift in Sources */, 0CF2C2DD2A783CE600FDC837 /* ErrorView.swift in Sources */, + 79641C212DEF1ED7004C970D /* RatingList.swift in Sources */, 5019A4CE2DE1E899009A5AF8 /* FakeNftApp.swift in Sources */, + 79641C1C2DEF1E17004C970D /* RatingRow.swift in Sources */, + 79641C282DEF293A004C970D /* UserCardView.swift in Sources */, 0CFCB7402A78002A0009A829 /* ExamplePutService.swift in Sources */, + 797AAE882DEF30E400745B0D /* UsersService.swift in Sources */, 3F6806D529CBBEC700B4F915 /* NetworkTask.swift in Sources */, 558E39E72C68CE0A00FB86AC /* NftService.swift in Sources */, 0C79EE6C2A76DE2E00EE90EA /* ServicesAssemly.swift in Sources */, @@ -509,6 +596,7 @@ 0CFCB74E2A7817DC0009A829 /* NftStorage.swift in Sources */, 3FC8C39129D2453B0081F015 /* ExamplePutRequest.swift in Sources */, 5019A4D02DE1E8E0009A5AF8 /* ContentView.swift in Sources */, + 79641C232DEF2081004C970D /* RatingViewModel.swift in Sources */, 0C79EE612A76DCD600EE90EA /* NftByIdRequest.swift in Sources */, 3F6806D329CBBE9600B4F915 /* NetworkRequest.swift in Sources */, 3F6806D129CBBE6B00B4F915 /* NetworkClient.swift in Sources */, diff --git a/FakeNFT/Docs/Statistics.md b/FakeNFT/Docs/Statistics.md index 34b26815ed..b0f501694b 100644 --- a/FakeNFT/Docs/Statistics.md +++ b/FakeNFT/Docs/Statistics.md @@ -14,17 +14,17 @@ ## Модуль 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: 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/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/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/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..d67a901edf --- /dev/null +++ b/FakeNFT/Scenes /Statistics/RatingView/RatingView.swift @@ -0,0 +1,89 @@ +// +// 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(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..d73a4e3d22 --- /dev/null +++ b/FakeNFT/Scenes /Statistics/UserCardView/UserCardView.swift @@ -0,0 +1,37 @@ +// +// UserCardView.swift +// FakeNFT +// +// Created by Anastasia on 03.06.2025. +// + +import SwiftUI + +struct UserCardView: View { + + @Binding var isTabBarHidden: Bool + + // TODO: 2/3 Statistics + + var body: some View { + VStack { + Text("Hello, World!") + } + .modifier(NavigationBarStyle( + title: nil, + backButtonHidden: false, + filterButtonHidden: true, + isTabBarHidden: $isTabBarHidden, + filterButtonTapHandler: { } + )) + .onAppear { + isTabBarHidden = true + } + } +} + +#Preview { + NavigationView { + UserCardView(isTabBarHidden: .constant(true)) + } +} 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/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..ae48e606c8 100644 --- a/FakeNFT/Services/ServicesAssemly.swift +++ b/FakeNFT/Services/ServicesAssemly.swift @@ -19,4 +19,8 @@ final class ServicesAssembly: ObservableObject { storage: nftStorage ) } + + var usersService: UsersService { + UsersServiceImpl(networkClient: networkClient) + } } 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..fa7c8e02e2 --- /dev/null +++ b/FakeNFT/StatisticsConstants.swift @@ -0,0 +1,23 @@ +// +// 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 +}