From c550321886fdb080d59e79baa2ba42a77aee25ca Mon Sep 17 00:00:00 2001 From: danilpapa Date: Thu, 21 May 2026 11:58:39 +0300 Subject: [PATCH] setup both flow --- .../mobile/profile/EditProfileScreen.kt | 131 ++++++++---------- .../friends/mobile/profile/ProfileScreen.kt | 110 ++++++++------- iosApp/iosApp/DesignKit/DesignTheme.swift | 4 + .../Profile/BusyDays/BusyDayView.swift | 114 +++++++-------- .../Profile/BusyDays/BusyDaysReducer.swift | 4 + .../Profile/EditProfile/EditProfileView.swift | 43 +++--- .../iosApp/Modules/Profile/ProfileView.swift | 59 +++++--- .../Profile/WishPlaces/WishPlaceItem.swift | 99 +++---------- .../Profile/WishPlaces/WishPlacesView.swift | 117 ++++++++-------- 9 files changed, 335 insertions(+), 346 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt index 5b8ce57..e18d92a 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt @@ -1,6 +1,5 @@ package friends.mobile.profile -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,20 +8,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Person -import androidx.compose.material3.Button import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,9 +23,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import friends.mobile.designkit.DesignTheme +import friends.mobile.designkit.FormTextField +import friends.mobile.designkit.PrimaryButton import friends.mobile.feature.profile.presentation.edit.EditProfileAction import friends.mobile.feature.profile.presentation.edit.EditProfileEvent import friends.mobile.feature.profile.presentation.edit.EditProfileViewModel @@ -101,71 +96,61 @@ private fun EditContent( state: EditProfileViewState.Content, onEvent: (EditProfileEvent) -> Unit ) { - Column( + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + .padding(DesignTheme.Spacing.lg), + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.lg) ) { - // Avatar Placeholder - Box( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.Person, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant + item { + FormTextField( + value = state.username, + onValueChange = { onEvent(EditProfileEvent.OnUsernameChanged(it)) }, + placeholder = "Username", + modifier = Modifier + .fillMaxWidth() + .height(54.dp) ) } - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = state.username, - onValueChange = { onEvent(EditProfileEvent.OnUsernameChanged(it)) }, - label = { Text("Username") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) + item { + FormTextField( + value = state.bio, + onValueChange = { onEvent(EditProfileEvent.OnBioChanged(it)) }, + placeholder = "Bio", + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + ) + } - OutlinedTextField( - value = state.avatarUrl, - onValueChange = { onEvent(EditProfileEvent.OnAvatarUrlChanged(it)) }, - label = { Text("Avatar URL") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) + item { + FormTextField( + value = state.avatarUrl, + onValueChange = { onEvent(EditProfileEvent.OnAvatarUrlChanged(it)) }, + placeholder = "Avatar URL", + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + ) + } - OutlinedTextField( - value = state.bio, - onValueChange = { onEvent(EditProfileEvent.OnBioChanged(it)) }, - label = { Text("Bio") }, - modifier = Modifier.fillMaxWidth(), - minLines = 3 - ) + item { + Spacer(modifier = Modifier.height(DesignTheme.Spacing.xxxl)) + } - Spacer(modifier = Modifier.weight(1f)) + item { + PrimaryButton( + text = if (state.isSaving) "Saving..." else "Save", + onClick = { onEvent(EditProfileEvent.OnSaveClick) }, + modifier = Modifier.fillMaxWidth(), + isLoading = state.isSaving, + isEnabled = !state.isSaving + ) + } - Button( - onClick = { onEvent(EditProfileEvent.OnSaveClick) }, - modifier = Modifier.fillMaxWidth().height(50.dp), - enabled = !state.isSaving - ) { - if (state.isSaving) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text("Save Changes") - } + item { + Spacer(modifier = Modifier.height(DesignTheme.Spacing.lg)) } } } @@ -173,18 +158,22 @@ private fun EditContent( @Composable private fun ErrorState(message: String, onBack: () -> Unit) { Column( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(DesignTheme.Spacing.lg), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyLarge + color = DesignTheme.Colors.error, + style = DesignTheme.Typography.body + ) + Spacer(modifier = Modifier.height(DesignTheme.Spacing.lg)) + PrimaryButton( + text = "Go Back", + onClick = onBack, + modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onBack) { - Text("Go Back") - } } } diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt index 6afbddf..8ebdfed 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt @@ -12,11 +12,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -28,10 +29,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import friends.mobile.calendar.BusyDaysView +import friends.mobile.designkit.DesignTheme +import friends.mobile.designkit.PrimaryButton import friends.mobile.feature.profile.domain.model.Profile import friends.mobile.feature.profile.presentation.profile.ProfileAction import friends.mobile.feature.profile.presentation.profile.ProfileEvent @@ -162,8 +164,8 @@ private fun ProfileContent( LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) + contentPadding = PaddingValues(DesignTheme.Spacing.lg), + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.xxl) ) { item { @@ -177,18 +179,35 @@ private fun ProfileContent( } item { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) + ) { + PrimaryButton( + text = "Refresh Activity", + onClick = { }, + modifier = Modifier.fillMaxWidth(), + icon = { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh", + modifier = Modifier.size(20.dp) + ) + } + ) - BusyDaysView( - userId = profile.id, - modifier = Modifier.fillMaxWidth() - ) + BusyDaysView( + userId = profile.id, + modifier = Modifier.fillMaxWidth() + ) + } } item { Text( text = "My Wish Places", - style = MaterialTheme.typography.titleLarge + style = DesignTheme.Typography.heading ) } @@ -202,7 +221,7 @@ private fun ProfileContent( } item { - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(DesignTheme.Spacing.xxxl)) } } } @@ -218,58 +237,41 @@ private fun ProfileHeader( Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { Text( text = profile.username, - style = MaterialTheme.typography.headlineMedium + style = DesignTheme.Typography.heading ) profile.bio?.let { bio -> Text( text = bio, - style = MaterialTheme.typography.bodyLarge + style = DesignTheme.Typography.body ) } - Row( + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { - Button( + PrimaryButton( onClick = onEditClick, - modifier = Modifier.weight(1f), - enabled = !isLoggingOut - ) { - - Text("Edit Profile") - } + text = "Edit Profile", + modifier = Modifier.fillMaxWidth(), + isEnabled = !isLoggingOut + ) - Button( + PrimaryButton( onClick = onLogoutClick, - modifier = Modifier.weight(1f), - enabled = !isLoggingOut, - - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - - if (isLoggingOut) { - - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = Color.White - ) - - } else { - - Text("Logout") - } - } + text = if (isLoggingOut) "Logging out..." else "Log Out", + modifier = Modifier.fillMaxWidth(), + isLoading = isLoggingOut, + isEnabled = !isLoggingOut + ) } } } @@ -281,23 +283,25 @@ private fun ErrorContent( ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(DesignTheme.Spacing.lg), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = message, - color = MaterialTheme.colorScheme.error + style = DesignTheme.Typography.body, + color = DesignTheme.Colors.error ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(DesignTheme.Spacing.lg)) - Button( - onClick = onRetry - ) { - - Text("Retry") - } + PrimaryButton( + text = "Retry", + onClick = onRetry, + modifier = Modifier.fillMaxWidth() + ) } } diff --git a/iosApp/iosApp/DesignKit/DesignTheme.swift b/iosApp/iosApp/DesignKit/DesignTheme.swift index 16d5a04..95da2cb 100644 --- a/iosApp/iosApp/DesignKit/DesignTheme.swift +++ b/iosApp/iosApp/DesignKit/DesignTheme.swift @@ -11,6 +11,10 @@ import UIKit enum DesignTheme { static let accentColor = Color.blue static let accentColorHex = Color(red: 0.0, green: 0.48, blue: 1.0) + static let error = Color(red: 1.0, green: 0.23, blue: 0.19) + static let errorLight = Color.red.opacity(0.08) + static let infoLight = Color.blue.opacity(0.06) + static let secondaryAccent = Color.green enum Spacing { static let xs: CGFloat = 4 diff --git a/iosApp/iosApp/Modules/Profile/BusyDays/BusyDayView.swift b/iosApp/iosApp/Modules/Profile/BusyDays/BusyDayView.swift index 76169fa..78924fe 100644 --- a/iosApp/iosApp/Modules/Profile/BusyDays/BusyDayView.swift +++ b/iosApp/iosApp/Modules/Profile/BusyDays/BusyDayView.swift @@ -10,90 +10,94 @@ import Foundation import Shared struct BusyDayView: View { - + @State private var reducer: BusyDaysReducer private let userId: String - + init(userId: String) { self.userId = userId let reducer = BusyDaysReducer(userId: userId) self._reducer = State(initialValue: reducer) } - + var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text("Activity") - .font(.headline) - .fontWeight(.semibold) - - ZStack { - if reducer.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: 120, alignment: .center) - } else if let errorMessage = reducer.errorMessage { - VStack(spacing: 12) { + VStack(alignment: .leading, spacing: DesignTheme.Spacing.lg) { + if reducer.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: 120, alignment: .center) + } else if let errorMessage = reducer.errorMessage { + VStack(spacing: DesignTheme.Spacing.md) { + HStack(spacing: DesignTheme.Spacing.sm) { Image(systemName: "exclamationmark.circle") .font(.title3) - .foregroundColor(.orange) + .foregroundColor(DesignTheme.error) Text(errorMessage) - .font(.caption) + .font(DesignTheme.Typography.caption) .foregroundColor(.secondary) - Button("Retry") { - reducer.retry() - } - .tint(.blue) - .font(.caption) } - .padding() - .frame(maxWidth: .infinity) - } else if reducer.busyDays.isEmpty { - HStack(spacing: 8) { - Image(systemName: "calendar") - .foregroundColor(.gray) - Text("No busy days recorded") - .foregroundColor(.gray) - .font(.caption) + Button(action: { reducer.retry() }) { + Text("Retry") + .font(DesignTheme.Typography.caption) + .frame(maxWidth: .infinity) + .frame(height: 36) + .background(DesignTheme.accentColor) + .foregroundColor(.white) + .cornerRadius(DesignTheme.CornerRadius.medium) } - .padding() - .frame(maxWidth: .infinity, alignment: .center) - } else { - ActivityGridView(busyDays: reducer.busyDays) } + .padding(DesignTheme.Spacing.lg) + .frame(maxWidth: .infinity) + .background(DesignTheme.errorLight) + .cornerRadius(DesignTheme.CornerRadius.medium) + } else if reducer.busyDays.isEmpty { + HStack(spacing: DesignTheme.Spacing.sm) { + Image(systemName: "calendar") + .foregroundColor(.gray) + Text("No busy days recorded") + .font(DesignTheme.Typography.caption) + .foregroundColor(.gray) + } + .padding(DesignTheme.Spacing.lg) + .frame(maxWidth: .infinity, alignment: .center) + .background(Color.gray.opacity(0.05)) + .cornerRadius(DesignTheme.CornerRadius.medium) + } else { + ActivityGridView(busyDays: reducer.busyDays) + .padding(DesignTheme.Spacing.lg) + .background(Color.gray.opacity(0.03)) + .cornerRadius(DesignTheme.CornerRadius.medium) } - .frame(maxWidth: .infinity) } - .padding() } } struct ActivityGridView: View { - + let busyDays: [String] - + var body: some View { let calendar = Calendar.current let today = Date() let endDate = calendar.date(byAdding: .day, value: 30, to: today) ?? today - + let gridSize: Int = 7 let startDate = today let dateRange = datesInRange(from: startDate, to: endDate) - - let columns = Array(repeating: GridItem(.flexible(), spacing: 4), count: gridSize) - - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 4) { + + let columns = Array(repeating: GridItem(.flexible(), spacing: DesignTheme.Spacing.xs), count: gridSize) + + VStack(alignment: .leading, spacing: DesignTheme.Spacing.md) { + HStack(spacing: DesignTheme.Spacing.xs) { ForEach(["S", "M", "T", "W", "T", "F", "S"], id: \.self) { day in Text(day) - .font(.caption2) - .fontWeight(.semibold) + .font(DesignTheme.Typography.bodySmallest) .foregroundColor(.secondary) .frame(maxWidth: .infinity) } } - .padding(.bottom, 4) - - LazyVGrid(columns: columns, spacing: 4) { + .padding(.bottom, DesignTheme.Spacing.xs) + + LazyVGrid(columns: columns, spacing: DesignTheme.Spacing.xs) { ForEach(dateRange, id: \.self) { date in DayCell(date: date, isBusy: isBusy(date)) } @@ -123,23 +127,23 @@ struct ActivityGridView: View { } struct DayCell: View { - + let date: Date let isBusy: Bool - + var body: some View { VStack { Text(dayOfMonth) - .font(.caption2) + .font(DesignTheme.Typography.bodySmallest) .fontWeight(.semibold) .foregroundColor(isBusy ? .white : .primary) .frame(maxWidth: .infinity, maxHeight: .infinity) } .aspectRatio(1, contentMode: .fit) - .background(isBusy ? Color.green : Color.gray.opacity(0.15)) - .cornerRadius(4) + .background(isBusy ? DesignTheme.secondaryAccent : Color.gray.opacity(0.1)) + .cornerRadius(DesignTheme.CornerRadius.small) } - + private var dayOfMonth: String { let formatter = DateFormatter() formatter.dateFormat = "d" diff --git a/iosApp/iosApp/Modules/Profile/BusyDays/BusyDaysReducer.swift b/iosApp/iosApp/Modules/Profile/BusyDays/BusyDaysReducer.swift index ba24dcc..859f398 100644 --- a/iosApp/iosApp/Modules/Profile/BusyDays/BusyDaysReducer.swift +++ b/iosApp/iosApp/Modules/Profile/BusyDays/BusyDaysReducer.swift @@ -53,4 +53,8 @@ final class BusyDaysReducer { func retry() { sharedVM.obtainEvent(event: BusyDaysEvent.OnRetry()) } + + func refresh() { + sharedVM.obtainEvent(event: BusyDaysEvent.OnRetry()) + } } diff --git a/iosApp/iosApp/Modules/Profile/EditProfile/EditProfileView.swift b/iosApp/iosApp/Modules/Profile/EditProfile/EditProfileView.swift index e735b42..4921cfd 100644 --- a/iosApp/iosApp/Modules/Profile/EditProfile/EditProfileView.swift +++ b/iosApp/iosApp/Modules/Profile/EditProfile/EditProfileView.swift @@ -10,54 +10,59 @@ import Shared struct EditProfileView: View { @Environment(Router.self) private var router - + @State private var reducer: EditProfileReducer - + init(profile: Profile) { self.reducer = EditProfileReducer(profile: profile) } - + var body: some View { - VStack(spacing: 16) { + VStack(spacing: DesignTheme.Spacing.lg) { TextField("Username", text: Binding( get: { reducer.username }, set: { reducer.updateUsername($0) } )) - .textFieldStyle(.roundedBorder) - + .formTextField() + TextField("Bio", text: Binding( get: { reducer.bio }, set: { reducer.updateBio($0) } )) - .textFieldStyle(.roundedBorder) - .lineLimit(3...5) - + .formTextField() + TextField("Avatar URL", text: Binding( get: { reducer.avatarUrl }, set: { reducer.updateAvatarUrl($0) } )) - .textFieldStyle(.roundedBorder) - + .formTextField() + if let error = reducer.saveError { Text(error) - .font(.caption) - .foregroundColor(.red) + .font(DesignTheme.Typography.caption) + .foregroundColor(DesignTheme.error) } - + Spacer() - - Button { - reducer.save() - } label: { + + Button(action: { reducer.save() }) { if reducer.isSaving { ProgressView() .progressViewStyle(.circular) + .tint(.white) } else { Text("Save") + .font(DesignTheme.Typography.button) } } - .tint(reducer.isSaving ? .gray : .green) + .frame(maxWidth: .infinity) + .frame(height: 54) + .foregroundColor(.white) + .background(DesignTheme.accentColor) + .cornerRadius(DesignTheme.CornerRadius.capsule) + .disabled(reducer.isSaving) } + .padding(DesignTheme.Spacing.lg) .navigationTitle("Edit Profile") .navigationBarTitleDisplayMode(.inline) .task { diff --git a/iosApp/iosApp/Modules/Profile/ProfileView.swift b/iosApp/iosApp/Modules/Profile/ProfileView.swift index 7cf18bc..bf8bd56 100644 --- a/iosApp/iosApp/Modules/Profile/ProfileView.swift +++ b/iosApp/iosApp/Modules/Profile/ProfileView.swift @@ -9,40 +9,65 @@ import SwiftUI import Shared struct ProfileView: View { - + @Environment(Router.self) private var router @State private var profileReducer = ProfileReducer() @State private var showEditProfile = false - + var body: some View { if let profile = profileReducer.profile { ScrollView { - VStack(spacing: 24) { + VStack(spacing: DesignTheme.Spacing.xxl) { UserView(user: profile, dimension: .vertical) .frame(maxWidth: .infinity) - BusyDayView(userId: profile.id) - .frame(maxWidth: .infinity, alignment: .topLeading) + + VStack(spacing: DesignTheme.Spacing.md) { + Button(action: { profileReducer.loadProfile() }) { + HStack(spacing: DesignTheme.Spacing.sm) { + Image(systemName: "arrow.clockwise") + Text("Refresh Activity") + } + .font(DesignTheme.Typography.button) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(DesignTheme.accentColor) + .foregroundColor(.white) + .cornerRadius(DesignTheme.CornerRadius.capsule) + } + + BusyDayView(userId: profile.id) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + WishPlacesView(userId: profile.id, mode: .editable) .frame(maxWidth: .infinity, alignment: .topLeading) - VStack(spacing: 12) { - Button("Edit Profile") { - profileReducer.navigateToEdit() + + VStack(spacing: DesignTheme.Spacing.md) { + Button(action: { profileReducer.navigateToEdit() }) { + Text("Edit Profile") + .font(DesignTheme.Typography.button) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(DesignTheme.accentColor) + .foregroundColor(.white) + .cornerRadius(DesignTheme.CornerRadius.capsule) } - .tint(.blue) - .frame(maxWidth: .infinity) - - Button("Log Out") { - profileReducer.logout() + + Button(action: { profileReducer.logout() }) { + Text("Log Out") + .font(DesignTheme.Typography.button) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(DesignTheme.error) + .foregroundColor(.white) + .cornerRadius(DesignTheme.CornerRadius.capsule) } - .tint(.red) - .frame(maxWidth: .infinity) } } - .padding() + .padding(DesignTheme.Spacing.lg) } .navigationTitle("Profile") .onAppear { - // TODO: костыль обновления profileReducer.loadProfile() } .task { diff --git a/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlaceItem.swift b/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlaceItem.swift index bd0e799..acb8ef5 100644 --- a/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlaceItem.swift +++ b/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlaceItem.swift @@ -9,98 +9,45 @@ import SwiftUI import Shared struct WishPlaceItem: View { - + let place: Shared.WishPlace - let canDelete: Bool let onTap: () -> Void - let onDelete: () -> Void - - @State private var dismissOffset: CGFloat = 0 - private let dismissThreshold: CGFloat = UIScreen.main.bounds.width * 0.3 - + var body: some View { - ZStack(alignment: .trailing) { - if canDelete { - HStack(spacing: 0) { - Spacer() - VStack { - Image(systemName: "trash.fill") - .font(.system(size: 18)) - .foregroundColor(.red) - } - .background(Color.clear) - } - } - - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .top, spacing: 12) { - VStack(alignment: .leading, spacing: 4) { + Button(action: onTap) { + VStack(alignment: .leading, spacing: DesignTheme.Spacing.sm) { + HStack(alignment: .top, spacing: DesignTheme.Spacing.md) { + VStack(alignment: .leading, spacing: DesignTheme.Spacing.xs) { Text(place.title) - .font(.system(.body, design: .default)) + .font(DesignTheme.Typography.body) .fontWeight(.semibold) - .lineLimit(2) - + .lineLimit(1) + if let location = place.location { - HStack(spacing: 4) { - Image(systemName: "location.fill") - .font(.system(size: 12)) - .foregroundColor(.blue) - Text(location) - .font(.caption) - .foregroundColor(.gray) - .lineLimit(1) - } + Text(location) + .font(DesignTheme.Typography.caption) + .foregroundColor(.gray) + .lineLimit(1) } } - + Spacer() - + Text(place.status.name.lowercased().prefix(1).uppercased() + place.status.name.lowercased().dropFirst()) - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(.blue) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue.opacity(0.1)) - .cornerRadius(4) + .font(DesignTheme.Typography.bodySmallest) + .foregroundColor(DesignTheme.accentColor) } - + if let description = place.description_ { Text(description) - .font(.caption) + .font(DesignTheme.Typography.caption) .foregroundColor(.gray) - .lineLimit(2) + .lineLimit(1) } } - .padding(12) - .background(Color(.systemGray6)) - .cornerRadius(12) - .offset(x: dismissOffset) - .gesture( - canDelete ? DragGesture() - .onChanged { gesture in - let translation = gesture.translation.width - if translation < 0 { - dismissOffset = min(0, translation) - } - } - .onEnded { gesture in - if gesture.translation.width < -dismissThreshold { - dismissOffset = 0 - onDelete() - } else { - withAnimation(.easeOut(duration: 0.2)) { - dismissOffset = 0 - } - } - } - : nil - ) - .onTapGesture { - onTap() - } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } - .clipped() - .cornerRadius(12) + .foregroundColor(.primary) } } diff --git a/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlacesView.swift b/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlacesView.swift index 7c3fa0c..ac0e7a6 100644 --- a/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlacesView.swift +++ b/iosApp/iosApp/Modules/Profile/WishPlaces/WishPlacesView.swift @@ -22,78 +22,85 @@ struct WishPlacesView: View { } var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(mode == .readOnly ? "Wish Places" : "My Wish Places") - .font(.headline) - .fontWeight(.semibold) - - if mode == .editable { - Button { - reducer.showCreateSheet = true - } label: { - HStack { + VStack(alignment: .leading, spacing: DesignTheme.Spacing.lg) { + HStack { + Text(mode == .readOnly ? "Wish Places" : "My Wish Places") + .font(DesignTheme.Typography.heading) + + Spacer() + + if mode == .editable { + Button(action: { reducer.showCreateSheet = true }) { Image(systemName: "plus.circle.fill") - Text("Create Wish Place") + .font(.system(size: 24)) + .foregroundColor(DesignTheme.accentColor) } } } - - ZStack { - if reducer.isLoading { - ProgressView() - } else if let errorMessage = reducer.errorMessage { - VStack(spacing: 12) { - Text(errorMessage) - .foregroundColor(.red) - Button("Retry") { - reducer.retry() - } - .tint(.blue) + + if reducer.isLoading { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + } else if let errorMessage = reducer.errorMessage { + VStack(spacing: DesignTheme.Spacing.md) { + Text(errorMessage) + .font(DesignTheme.Typography.caption) + .foregroundColor(DesignTheme.error) + Button(action: { reducer.retry() }) { + Text("Retry") + .font(DesignTheme.Typography.button) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(DesignTheme.accentColor) + .foregroundColor(.white) + .cornerRadius(DesignTheme.CornerRadius.capsule) } - .padding() - .frame(maxWidth: .infinity) - } else if reducer.places.isEmpty { - Text("No wish places yet") - .foregroundColor(.gray) - .padding() - } else { - LazyVStack { - ForEach(reducer.places, id: \.id) { place in - WishPlaceItem( - place: place, - canDelete: mode == .editable, - onTap: { - reducer.selectedPlace = place - }, - onDelete: { - withAnimation { - reducer.deletePlace(id: place.id) - } - } - ) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - .listStyle(.plain) - } - .onDelete { indexSet in - reducer.places.remove(atOffsets: indexSet) + } + } else if reducer.places.isEmpty { + Text("No wish places yet") + .font(DesignTheme.Typography.caption) + .foregroundColor(.gray) + .frame(maxWidth: .infinity, alignment: .center) + .padding(DesignTheme.Spacing.lg) + } else { + List { + ForEach(reducer.places, id: \.id) { place in + WishPlaceItem( + place: place, + onTap: { + reducer.selectedPlace = place + } + ) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: DesignTheme.Spacing.sm, leading: 0, bottom: DesignTheme.Spacing.sm, trailing: 0)) + } + .onDelete { indexSet in + for index in indexSet { + withAnimation { + reducer.deletePlace(id: reducer.places[index].id) + } } } } + .listStyle(.plain) + .scrollDisabled(true) + .frame(height: CGFloat(reducer.places.count) * 120) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - + if reducer.isActionPending { - HStack(spacing: 8) { + HStack(spacing: DesignTheme.Spacing.sm) { ProgressView() .scaleEffect(0.8) Text("Processing...") - .font(.caption) + .font(DesignTheme.Typography.caption) .foregroundColor(.gray) } } + + Spacer() } - .padding() + .padding(DesignTheme.Spacing.lg) .sheet(isPresented: $reducer.showCreateSheet) { CreateWishPlaceSheet( onDismiss: {