Skip to content

Commit d8f4fcd

Browse files
committed
Add profile edit support
1 parent 2d72105 commit d8f4fcd

File tree

8 files changed

+867
-0
lines changed

8 files changed

+867
-0
lines changed

Modules/Sources/AnalyticsClient/Events/ProfileEvent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99

1010
public enum ProfileEvent: Event {
1111
case qmsTapped
12+
case editTapped
1213
case settingsTapped
1314
case logoutTapped
1415
case historyTapped

Modules/Sources/Models/Resources/Localizable.xcstrings

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
}
4242
}
4343
},
44+
"Female" : {
45+
"localizations" : {
46+
"ru" : {
47+
"stringUnit" : {
48+
"state" : "translated",
49+
"value" : "Женский"
50+
}
51+
}
52+
}
53+
},
4454
"First page" : {
4555
"localizations" : {
4656
"ru" : {
@@ -71,6 +81,16 @@
7181
}
7282
}
7383
},
84+
"Male" : {
85+
"localizations" : {
86+
"ru" : {
87+
"stringUnit" : {
88+
"state" : "translated",
89+
"value" : "Мужской"
90+
}
91+
}
92+
}
93+
},
7494
"No comment text" : {
7595
"localizations" : {
7696
"ru" : {
@@ -81,6 +101,16 @@
81101
}
82102
}
83103
},
104+
"Not set" : {
105+
"localizations" : {
106+
"ru" : {
107+
"stringUnit" : {
108+
"state" : "translated",
109+
"value" : "Не указан"
110+
}
111+
}
112+
}
113+
},
84114
"Profile" : {
85115
"localizations" : {
86116
"ru" : {

Modules/Sources/ProfileFeature/Analytics/ProfileFeature+Analytics.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ extension ProfileFeature {
2929
case .view(.qmsButtonTapped):
3030
analyticsClient.log(ProfileEvent.qmsTapped)
3131

32+
case .view(.editButtonTapped):
33+
analyticsClient.log(ProfileEvent.editTapped)
34+
3235
case .view(.settingsButtonTapped):
3336
analyticsClient.log(ProfileEvent.settingsTapped)
3437

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
//
2+
// EditFeature.swift
3+
// ForPDA
4+
//
5+
// Created by Xialtal on 28.08.25.
6+
//
7+
8+
import Foundation
9+
import ComposableArchitecture
10+
import APIClient
11+
import Models
12+
import ToastClient
13+
14+
@Reducer
15+
public struct EditFeature: Reducer, Sendable {
16+
17+
public init() {}
18+
19+
// MARK: - Localizations
20+
21+
private enum Localization {
22+
static let avatarUpdated = LocalizedStringResource("Avatar updated", bundle: .module)
23+
static let avatarUpdateError = LocalizedStringResource("Avatar update error", bundle: .module)
24+
static let avatarFileSizeError = LocalizedStringResource("Avatar size more than 32KB", bundle: .module)
25+
static let avatarWidthHeightError = LocalizedStringResource("Avatar must be 100x100", bundle: .module)
26+
}
27+
28+
// MARK: - Destinations
29+
30+
@Reducer(state: .equatable)
31+
public enum Destination: Hashable, Equatable {
32+
case alert(AlertState<Alert>)
33+
case avatarPicker
34+
35+
public enum Alert {
36+
case yes, no
37+
}
38+
}
39+
40+
// MARK: - State
41+
42+
@ObservableState
43+
public struct State: Equatable {
44+
@Presents public var destination: Destination.State?
45+
46+
let user: User
47+
var draftUser: User
48+
var avatarReloadId = UUID()
49+
50+
var isSending = false
51+
var isAvatarUploading = false
52+
53+
var birthdayDate: Date?
54+
55+
var isUserSetAvatar: Bool {
56+
return draftUser.imageUrl != Links.defaultAvatar
57+
}
58+
59+
var isUserSetBirhdayDate: Bool {
60+
return user.birthdayDate != nil
61+
}
62+
63+
var isUserCanEditStatus: Bool {
64+
return user.posts >= 250
65+
}
66+
67+
var isSavingDisabled: Bool {
68+
return isUserInfoFieldsEqual
69+
&& draftUser.devDBdevices == user.devDBdevices
70+
}
71+
72+
var isUserInfoFieldsEqual: Bool {
73+
return draftUser.city == user.city
74+
&& draftUser.aboutMe == user.aboutMe
75+
&& draftUser.status == user.status
76+
&& draftUser.gender == user.gender
77+
&& draftUser.signature == user.signature
78+
&& draftUser.birthdayDate == birthdayDate
79+
}
80+
81+
public init(user: User) {
82+
self.user = user
83+
self.draftUser = user
84+
}
85+
}
86+
87+
// MARK: - Action
88+
89+
public enum Action: BindableAction, ViewAction {
90+
case binding(BindingAction<State>)
91+
case destination(PresentationAction<Destination.Action>)
92+
93+
case view(View)
94+
public enum View {
95+
case onAppear
96+
case avatarSelected(Data)
97+
case deleteAvatar
98+
case avatarBadWidthHeight
99+
case avatarBadFileSizeTooBig
100+
101+
case wipeBirthdayDate
102+
case setBirthdayDate
103+
case selectGenderType(User.Gender)
104+
105+
case saveButtonTapped
106+
case cancelButtonTapped
107+
case addAvatarButtonTapped
108+
}
109+
110+
case `internal`(Internal)
111+
public enum Internal {
112+
case saveProfile
113+
case updateAvatar(Data)
114+
case updateAvatarResponse(Result<UserAvatarResponseType, any Error>)
115+
}
116+
117+
case delegate(Delegate)
118+
public enum Delegate {
119+
case profileUpdated(Bool)
120+
}
121+
}
122+
123+
// MARK: - Dependencies
124+
125+
@Dependency(\.dismiss) private var dismiss
126+
@Dependency(\.apiClient) private var apiClient
127+
@Dependency(\.toastClient) private var toastClient
128+
129+
// MARK: - Body
130+
131+
public var body: some Reducer<State, Action> {
132+
BindingReducer()
133+
134+
Reduce<State, Action> { state, action in
135+
switch action {
136+
case .binding:
137+
return .none
138+
139+
case .view(.onAppear):
140+
state.birthdayDate = state.draftUser.birthdayDate ?? nil
141+
return .none
142+
143+
case .view(.avatarSelected(let data)):
144+
return .send(.internal(.updateAvatar(data)))
145+
146+
case .view(.deleteAvatar):
147+
let empty = Data()
148+
return .send(.internal(.updateAvatar(empty)))
149+
150+
case .view(.avatarBadWidthHeight):
151+
return showToast(ToastMessage(text: Localization.avatarWidthHeightError, haptic: .error))
152+
153+
case .view(.avatarBadFileSizeTooBig):
154+
return showToast(ToastMessage(text: Localization.avatarFileSizeError, haptic: .error))
155+
156+
case .view(.selectGenderType(let type)):
157+
state.draftUser.gender = type
158+
return .none
159+
160+
case .view(.setBirthdayDate):
161+
state.birthdayDate = state.draftUser.birthdayDate ?? Date()
162+
return .none
163+
164+
case .view(.wipeBirthdayDate):
165+
state.birthdayDate = nil
166+
return .none
167+
168+
case .view(.saveButtonTapped):
169+
return .send(.internal(.saveProfile))
170+
171+
case .delegate(.profileUpdated):
172+
return .run { _ in await dismiss() }
173+
174+
case .view(.cancelButtonTapped):
175+
return .run { _ in await dismiss() }
176+
177+
case .view(.addAvatarButtonTapped):
178+
state.destination = .avatarPicker
179+
return .none
180+
181+
case .destination, .delegate:
182+
return .none
183+
184+
case .internal(.saveProfile):
185+
return .run { [user = state.draftUser, birthdayDate = state.birthdayDate] send in
186+
let status = try await apiClient.editUserProfile(UserProfileEditRequest(
187+
userId: user.id,
188+
city: user.city ?? "",
189+
about: user.aboutMe?.simplify() ?? "",
190+
gender: user.gender ?? .unknown,
191+
status: user.status ?? "",
192+
signature: user.signature?.simplify() ?? "",
193+
birthdayDate: birthdayDate
194+
))
195+
await send(.delegate(.profileUpdated(status)))
196+
}
197+
198+
case .internal(.updateAvatar(let data)):
199+
state.isAvatarUploading = true
200+
return .run { [userId = state.user.id] send in
201+
let response = try await apiClient.updateUserAvatar(userId, data)
202+
await send(.internal(.updateAvatarResponse(.success(response))))
203+
}
204+
205+
case let .internal(.updateAvatarResponse(.success(response))):
206+
switch response {
207+
case .success(let url):
208+
state.draftUser.imageUrl = url ?? Links.defaultAvatar
209+
state.isAvatarUploading = false
210+
211+
return showToast(ToastMessage(text: Localization.avatarUpdated, haptic: .success))
212+
case .error:
213+
return showToast(ToastMessage(text: Localization.avatarUpdateError, haptic: .error))
214+
}
215+
216+
case let .internal(.updateAvatarResponse(.failure(error))):
217+
print("Error: \(error)")
218+
return .none
219+
}
220+
}
221+
.ifLet(\.$destination, action: \.destination)
222+
}
223+
224+
private func showToast(_ toast: ToastMessage) -> Effect<Action> {
225+
return .run { _ in
226+
await toastClient.showToast(toast)
227+
}
228+
}
229+
}
230+
231+
private extension Date {
232+
func toProfileString() -> String {
233+
let dateFormatter = DateFormatter()
234+
dateFormatter.dateFormat = "dd-MM-yyyy"
235+
return dateFormatter.string(from: self)
236+
}
237+
}
238+
239+
private extension String {
240+
func simplify() -> String {
241+
return String(self.debugDescription.dropFirst().dropLast())
242+
}
243+
}

0 commit comments

Comments
 (0)