From 78c47819d01bd37a58bced5df188a547fff16e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSamoilenkoVadym=E2=80=9D?= <“samoylenko.vadym@gmail.com”> Date: Sun, 2 Mar 2025 20:09:58 +0000 Subject: [PATCH] Redesign dashboard with Apple-style UI improvements, better account cards and spending analysis --- Coinly/App/ContentView.swift | 9 +- .../Features/Accounts/AccountsListView.swift | 14 +- Coinly/Features/Dashboard/DashboardView.swift | 269 +++++++++++------- Coinly/Features/Models/AppSettings.swift | 28 +- Coinly/UI/Components/AccountCardView.swift | 18 +- .../UI/Components/AccountsSummaryView.swift | 30 +- 6 files changed, 223 insertions(+), 145 deletions(-) diff --git a/Coinly/App/ContentView.swift b/Coinly/App/ContentView.swift index 0c82bcb..093187b 100644 --- a/Coinly/App/ContentView.swift +++ b/Coinly/App/ContentView.swift @@ -26,11 +26,18 @@ struct ContentView: View { } } .environmentObject(settings) + .preferredColorScheme(settings.isDarkMode ? .dark : .light) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + Group { + ContentView() + .preferredColorScheme(.light) + + ContentView() + .preferredColorScheme(.dark) + } } } diff --git a/Coinly/Features/Accounts/AccountsListView.swift b/Coinly/Features/Accounts/AccountsListView.swift index 18b69da..73b3076 100644 --- a/Coinly/Features/Accounts/AccountsListView.swift +++ b/Coinly/Features/Accounts/AccountsListView.swift @@ -20,6 +20,7 @@ struct AccountsListView: View { Section(type.rawValue) { ForEach(store.getAccounts(of: type)) { account in AccountCardView(account: account) + .listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) } } } @@ -42,13 +43,20 @@ struct AccountsListView: View { } } } + .background(Color(uiColor: .systemGroupedBackground)) } } } struct AccountsListView_Previews: PreviewProvider { static var previews: some View { - AccountsListView(store: AccountsStore()) - .environmentObject(AppSettings.shared) + Group { + AccountsListView(store: AccountsStore()) + .preferredColorScheme(.light) + + AccountsListView(store: AccountsStore()) + .preferredColorScheme(.dark) + } + .environmentObject(AppSettings.shared) } -} \ No newline at end of file +} diff --git a/Coinly/Features/Dashboard/DashboardView.swift b/Coinly/Features/Dashboard/DashboardView.swift index 69c7c7f..0b53d50 100644 --- a/Coinly/Features/Dashboard/DashboardView.swift +++ b/Coinly/Features/Dashboard/DashboardView.swift @@ -4,6 +4,7 @@ struct DashboardView: View { @ObservedObject var store: TransactionsStore @ObservedObject var accountsStore: AccountsStore @EnvironmentObject private var settings: AppSettings + @State private var showingAccountsList = false private var totalBalance: Double { store.transactions.reduce(0) { total, transaction in @@ -42,129 +43,191 @@ struct DashboardView: View { var body: some View { NavigationView { ScrollView { - VStack(spacing: 20) { - // Balance Card - VStack(spacing: 8) { - Text("Total Balance") - .font(.subheadline) - .foregroundColor(.gray) - - Text(totalBalance.formatAsCurrency()) - .font(.title) - .fontWeight(.bold) - } - .frame(maxWidth: .infinity) - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(radius: 2) - - // Accounts Summary - AccountsSummaryView(accountsStore: accountsStore) - - // Income/Expense Summary - HStack(spacing: 16) { - // Income Card - VStack(spacing: 8) { - Text("Income") + VStack(spacing: 24) { + // Header Stats + HStack(spacing: 20) { + // Monthly Balance + VStack(alignment: .leading, spacing: 8) { + Text("This Month") .font(.subheadline) - .foregroundColor(.gray) - Text(income.formatAsCurrency()) - .font(.headline) - .foregroundColor(.green) - } - .frame(maxWidth: .infinity) - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(radius: 2) - - // Expense Card - VStack(spacing: 8) { - Text("Expenses") - .font(.subheadline) - .foregroundColor(.gray) - Text(expenses.formatAsCurrency()) - .font(.headline) - .foregroundColor(.red) - } - .frame(maxWidth: .infinity) - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(radius: 2) - } - - // Expenses by Category - if !categoryExpenses.isEmpty { - VStack(alignment: .leading, spacing: 16) { - Text("Expenses by Category") - .font(.headline) + .foregroundColor(Color(uiColor: .secondaryLabel)) + Text(totalBalance.formatAsCurrency()) + .font(.system(size: 28, weight: .semibold)) - // Pie Chart - PieChartView(slices: categoryExpenses) - .frame(height: 200) - .padding(.vertical) - - // Category Legend - VStack(spacing: 12) { - ForEach(categoryExpenses) { slice in - HStack { - Circle() - .fill(Color(slice.category.color)) - .frame(width: 12, height: 12) - - Text(slice.category.name) - - Spacer() - - Text(slice.amount.formatAsCurrency()) - .foregroundColor(.secondary) - - Text(String(format: "(%.1f%%)", slice.percentage * 100)) - .foregroundColor(.secondary) - } + HStack(spacing: 16) { + // Income indicator + HStack(spacing: 4) { + Circle() + .fill(Color.green) + .frame(width: 8, height: 8) + Text("↑ \(income.formatAsCurrency())") + .font(.caption) + .foregroundColor(.green) + } + + // Expense indicator + HStack(spacing: 4) { + Circle() + .fill(Color.red) + .frame(width: 8, height: 8) + Text("↓ \(expenses.formatAsCurrency())") + .font(.caption) + .foregroundColor(.red) } } } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(radius: 2) + .frame(maxWidth: .infinity, alignment: .leading) } + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(16) - // Recent Transactions - VStack(alignment: .leading, spacing: 12) { - Text("Recent Transactions") - .font(.headline) + // Accounts Section + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Accounts") + .font(.title2) + .fontWeight(.bold) + Spacer() + Button("See All") { + showingAccountsList = true + } + .foregroundColor(.accentColor) + } - ForEach(Array(store.transactions.prefix(3))) { transaction in - TransactionRowView(transaction: transaction) - if transaction.id != store.transactions.prefix(3).last?.id { - Divider() + ForEach(Array(accountsStore.accounts.prefix(3))) { account in + Button(action: { + // Show account details + }) { + HStack(spacing: 16) { + // Account Icon + Image(systemName: account.type.icon) + .font(.title2) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(accountColor(for: account.type)) + .cornerRadius(12) + + VStack(alignment: .leading, spacing: 4) { + Text(account.name) + .font(.headline) + .foregroundColor(Color(uiColor: .label)) + Text(account.type.rawValue) + .font(.caption) + .foregroundColor(Color(uiColor: .secondaryLabel)) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 4) { + Text(account.balance.formatAsCurrency()) + .font(.headline) + .foregroundColor(Color(uiColor: .label)) + if let availableCredit = account.availableCredit { + Text("Available: \(availableCredit.formatAsCurrency())") + .font(.caption) + .foregroundColor(Color(uiColor: .secondaryLabel)) + } + } + } + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(16) } } } - .frame(maxWidth: .infinity) - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(radius: 2) + .padding(.horizontal) + + // Expenses Analysis + VStack(alignment: .leading, spacing: 16) { + Text("Spending Analysis") + .font(.title2) + .fontWeight(.bold) + .padding(.horizontal) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(categoryExpenses) { slice in + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: slice.category.icon) + .foregroundColor(.white) + .frame(width: 32, height: 32) + .background(Color(slice.category.color)) + .cornerRadius(8) + + VStack(alignment: .leading) { + Text(slice.category.name) + .font(.headline) + Text(slice.amount.formatAsCurrency()) + .font(.subheadline) + .foregroundColor(Color(uiColor: .secondaryLabel)) + } + } + + // Progress bar + GeometryReader { geometry in + ZStack(alignment: .leading) { + Rectangle() + .fill(Color(uiColor: .systemFill)) + Rectangle() + .fill(Color(slice.category.color)) + .frame(width: geometry.size.width * slice.percentage) + } + } + .frame(height: 8) + .cornerRadius(4) + + Text(String(format: "%.1f%%", slice.percentage * 100)) + .font(.caption) + .foregroundColor(Color(uiColor: .secondaryLabel)) + } + .frame(width: 160) + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(16) + } + } + .padding(.horizontal) + } + } } - .padding() + .padding(.vertical) } .navigationTitle("Dashboard") - .background(Color(.systemGroupedBackground)) + .background(Color(uiColor: .systemBackground)) + .sheet(isPresented: $showingAccountsList) { + AccountsListView(store: accountsStore) + } + } + } + + private func accountColor(for type: AccountType) -> Color { + switch type { + case .wallet: return .blue + case .bankAccount: return .green + case .creditCard: return .purple + case .deposit: return .orange + case .debt: return .red } } } struct DashboardView_Previews: PreviewProvider { static var previews: some View { - DashboardView( - store: TransactionsStore(), - accountsStore: AccountsStore() - ) + Group { + DashboardView( + store: TransactionsStore(), + accountsStore: AccountsStore() + ) + .preferredColorScheme(.light) + + DashboardView( + store: TransactionsStore(), + accountsStore: AccountsStore() + ) + .preferredColorScheme(.dark) + } .environmentObject(AppSettings.shared) } } diff --git a/Coinly/Features/Models/AppSettings.swift b/Coinly/Features/Models/AppSettings.swift index df83304..8d8cf8e 100644 --- a/Coinly/Features/Models/AppSettings.swift +++ b/Coinly/Features/Models/AppSettings.swift @@ -1,4 +1,4 @@ -import Foundation +import SwiftUI class AppSettings: ObservableObject { @Published var currency: Currency = .usd { @@ -8,7 +8,11 @@ class AppSettings: ObservableObject { } } } - @Published var isDarkMode: Bool = false + @AppStorage("isDarkMode") var isDarkMode: Bool = false { + didSet { + applyTheme() + } + } @Published var notificationsEnabled: Bool = true @Published private(set) var exchangeRates: [String: Double] = [:] @Published private(set) var lastRatesUpdate: Date? @@ -30,13 +34,21 @@ class AppSettings: ObservableObject { static let shared = AppSettings() private init() { - // Инициализируем базовые курсы (1 USD = ...) exchangeRates = [ - "USD": 1.0, // 1 USD = 1 USD - "EUR": 0.92, // 1 USD = 0.92 EUR - "GBP": 0.79 // 1 USD = 0.79 GBP + "USD": 1.0, + "EUR": 0.92, + "GBP": 0.79 ] fetchExchangeRates() + applyTheme() + } + + private func applyTheme() { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = isDarkMode ? .dark : .light + } + } } func convert(_ amount: Double, from sourceCurrency: Currency, to targetCurrency: Currency) -> Double { @@ -47,13 +59,11 @@ class AppSettings: ObservableObject { return amount } - // Сначала конвертируем в USD, затем в целевую валюту if sourceCurrency == .usd { return amount * targetRate } else if targetCurrency == .usd { return amount / sourceRate } else { - // Конвертация через USD let amountInUSD = amount / sourceRate return amountInUSD * targetRate } @@ -73,7 +83,7 @@ class AppSettings: ObservableObject { let response = try JSONDecoder().decode(ExchangeRatesResponse.self, from: data) DispatchQueue.main.async { var rates = response.rates - rates["USD"] = 1.0 // Базовая валюта всегда 1.0 + rates["USD"] = 1.0 self?.exchangeRates = rates self?.lastRatesUpdate = Date() diff --git a/Coinly/UI/Components/AccountCardView.swift b/Coinly/UI/Components/AccountCardView.swift index 081bfd3..e22b5c1 100644 --- a/Coinly/UI/Components/AccountCardView.swift +++ b/Coinly/UI/Components/AccountCardView.swift @@ -1,11 +1,3 @@ -// -// AccountCardView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct AccountCardView: View { @@ -41,7 +33,7 @@ struct AccountCardView: View { Text(account.currency.symbol) .font(.subheadline) - .foregroundColor(.gray) + .foregroundColor(Color(uiColor: .secondaryLabel)) } // Balance @@ -53,7 +45,7 @@ struct AccountCardView: View { if let availableCredit = account.availableCredit { Text("Available: \(availableCredit.formatAsCurrency())") .font(.caption) - .foregroundColor(.gray) + .foregroundColor(Color(uiColor: .secondaryLabel)) } if account.isOverdue { @@ -63,7 +55,7 @@ struct AccountCardView: View { } } .padding() - .background(Color(.systemBackground)) + .background(Color(uiColor: .systemBackground)) .cornerRadius(12) .shadow(radius: 2) } @@ -76,8 +68,8 @@ struct AccountCardView_Previews: PreviewProvider { AccountCardView(account: AccountModel.sampleData[2]) } .padding() - .background(Color(.systemGroupedBackground)) + .background(Color(uiColor: .systemGroupedBackground)) .environmentObject(AppSettings.shared) .previewLayout(.sizeThatFits) } -} \ No newline at end of file +} diff --git a/Coinly/UI/Components/AccountsSummaryView.swift b/Coinly/UI/Components/AccountsSummaryView.swift index 7ef1ef3..373b076 100644 --- a/Coinly/UI/Components/AccountsSummaryView.swift +++ b/Coinly/UI/Components/AccountsSummaryView.swift @@ -1,11 +1,3 @@ -// -// AccountsSummaryView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct AccountsSummaryView: View { @@ -23,7 +15,7 @@ struct AccountsSummaryView: View { VStack(spacing: 8) { Text("Total Assets") .font(.subheadline) - .foregroundColor(.gray) + .foregroundColor(Color(uiColor: .secondaryLabel)) Text(totalBalance.formatAsCurrency()) .font(.title2) @@ -47,7 +39,7 @@ struct AccountsSummaryView: View { } } .padding() - .background(Color(.systemBackground)) + .background(Color(uiColor: .systemBackground)) .cornerRadius(12) .shadow(radius: 2) .sheet(isPresented: $showingAccountsList) { @@ -58,10 +50,16 @@ struct AccountsSummaryView: View { struct AccountsSummaryView_Previews: PreviewProvider { static var previews: some View { - AccountsSummaryView(accountsStore: AccountsStore()) - .environmentObject(AppSettings.shared) - .padding() - .background(Color(.systemGroupedBackground)) - .previewLayout(.sizeThatFits) + Group { + AccountsSummaryView(accountsStore: AccountsStore()) + .preferredColorScheme(.light) + + AccountsSummaryView(accountsStore: AccountsStore()) + .preferredColorScheme(.dark) + } + .environmentObject(AppSettings.shared) + .padding() + .background(Color(uiColor: .systemGroupedBackground)) + .previewLayout(.sizeThatFits) } -} \ No newline at end of file +}