From 474ed4906f0159ececdc3099e9e2c400e6d9a332 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 10:21:52 -0300 Subject: [PATCH 01/21] feat: date range selector sheet --- .../Activity/DateRangeSelectorSheet.swift | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 Bitkit/Components/Activity/DateRangeSelectorSheet.swift diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift new file mode 100644 index 00000000..367616fd --- /dev/null +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -0,0 +1,183 @@ + +import SwiftUI + +// MARK: - DateRangeSelectorSheet + +struct DateRangeSelectorSheet: View { + @Environment(\.dismiss) private var dismiss + @ObservedObject var viewModel: ActivityListViewModel + + @State private var selectedStartDate: Date? + @State private var selectedEndDate: Date? + @State private var isSelectingStart = true + + init(viewModel: ActivityListViewModel) { + self.viewModel = viewModel + _selectedStartDate = State(initialValue: viewModel.startDate) + _selectedEndDate = State(initialValue: viewModel.endDate) + } + + private var hasSelection: Bool { + selectedStartDate != nil && selectedEndDate != nil + } + + var body: some View { + VStack(spacing: 0) { + // Date Range Picker + VStack(alignment: .leading, spacing: 16) { + // Selection indicators + HStack(spacing: 12) { + DateSelectionButton( + title: t("wallet__filter_start_date"), + date: selectedStartDate, + isSelected: isSelectingStart, + action: { isSelectingStart = true } + ) + + DateSelectionButton( + title: t("wallet__filter_end_date"), + date: selectedEndDate, + isSelected: !isSelectingStart, + action: { isSelectingStart = false } + ) + } + .padding(.horizontal, 16) + .padding(.top, 20) + + // Calendar + DatePicker( + "", + selection: Binding( + get: { + if isSelectingStart { + return selectedStartDate ?? Date() + } else { + return selectedEndDate ?? Date() + } + }, + set: { newDate in + if isSelectingStart { + selectedStartDate = newDate + if selectedEndDate == nil || newDate > selectedEndDate! { + selectedEndDate = newDate + } + isSelectingStart = false + } else { + if let start = selectedStartDate, newDate < start { + selectedEndDate = start + selectedStartDate = newDate + } else { + selectedEndDate = newDate + } + } + } + ), + displayedComponents: .date + ) + .datePickerStyle(.graphical) + .tint(.brandAccent) + .padding(.horizontal, 16) + } + + Spacer() + + // Action buttons + HStack(spacing: 16) { + Button(action: { + selectedStartDate = nil + selectedEndDate = nil + viewModel.clearDateRange() + dismiss() + }) { + Text(t("wallet__filter_clear")) + .font(.system(size: 16, weight: .semibold)) + .frame(maxWidth: .infinity) + .frame(height: 50) + .foregroundColor(hasSelection ? .primary : .secondary) + .background( + RoundedRectangle(cornerRadius: 12) + .stroke(hasSelection ? Color.primary.opacity(0.3) : Color.secondary.opacity(0.2), lineWidth: 1) + ) + } + .disabled(!hasSelection) + .accessibilityIdentifier("CalendarClearButton") + + Button(action: { + viewModel.startDate = selectedStartDate + viewModel.endDate = selectedEndDate + dismiss() + }) { + Text(t("wallet__filter_apply")) + .font(.system(size: 16, weight: .semibold)) + .frame(maxWidth: .infinity) + .frame(height: 50) + .foregroundColor(.white) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(hasSelection ? Color.brandAccent : Color.gray.opacity(0.3)) + ) + } + .disabled(!hasSelection) + .accessibilityIdentifier("CalendarApplyButton") + } + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .background( + LinearGradient( + colors: [ + Color(red: 0.95, green: 0.95, blue: 0.97), + Color.white, + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .presentationDetents([.height(600)]) + .presentationDragIndicator(.visible) + } +} + +// MARK: - DateSelectionButton + +struct DateSelectionButton: View { + let title: String + let date: Date? + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 12)) + .foregroundColor(.secondary) + + Text(date?.formatted(date: .abbreviated, time: .omitted) ?? t("wallet__filter_select_date")) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(date == nil ? .secondary : .primary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(isSelected ? Color.brandAccent.opacity(0.1) : Color.clear) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? Color.brandAccent : Color.clear, lineWidth: 2) + ) + } + } +} + +#Preview("Empty State") { + DateRangeSelectorSheet(viewModel: ActivityListViewModel()) +} + +#Preview("With Selection") { + let viewModel = ActivityListViewModel() + viewModel.startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) + viewModel.endDate = Date() + return DateRangeSelectorSheet(viewModel: viewModel) +} From 85acedeafc945340396aaa3c01aa96ca9a145ff2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 10:34:19 -0300 Subject: [PATCH 02/21] fix: sheet background --- .../Components/Activity/ActivityListFilter.swift | 3 +-- .../Activity/DateRangeSelectorSheet.swift | 15 +-------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/Bitkit/Components/Activity/ActivityListFilter.swift b/Bitkit/Components/Activity/ActivityListFilter.swift index 949cdb42..1a5dbacd 100644 --- a/Bitkit/Components/Activity/ActivityListFilter.swift +++ b/Bitkit/Components/Activity/ActivityListFilter.swift @@ -1,7 +1,6 @@ import SwiftUI struct ActivityListFilter: View { - @EnvironmentObject private var sheets: SheetViewModel @ObservedObject var viewModel: ActivityListViewModel @State private var showingDateRange = false @State private var showingTagSelector = false @@ -52,7 +51,7 @@ struct ActivityListFilter: View { .background(Color.gray6) .cornerRadius(32) .sheet(isPresented: $showingDateRange) { - DateRangeSelector(viewModel: viewModel) + DateRangeSelectorSheet(viewModel: viewModel) } .sheet(isPresented: $showingTagSelector) { TagFilterSheet(viewModel: viewModel, isPresented: $showingTagSelector) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 367616fd..83736ce1 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -1,8 +1,6 @@ import SwiftUI -// MARK: - DateRangeSelectorSheet - struct DateRangeSelectorSheet: View { @Environment(\.dismiss) private var dismiss @ObservedObject var viewModel: ActivityListViewModel @@ -123,23 +121,12 @@ struct DateRangeSelectorSheet: View { .padding(.horizontal, 16) .padding(.bottom, 16) } - .background( - LinearGradient( - colors: [ - Color(red: 0.95, green: 0.95, blue: 0.97), - Color.white, - ], - startPoint: .top, - endPoint: .bottom - ) - ) + .sheetBackground() .presentationDetents([.height(600)]) .presentationDragIndicator(.visible) } } -// MARK: - DateSelectionButton - struct DateSelectionButton: View { let title: String let date: Date? From b6a2e4420c5f52d36cf2d76164b29c709306b0df Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 10:40:43 -0300 Subject: [PATCH 03/21] feat: use project buttons --- .../Activity/DateRangeSelectorSheet.swift | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 83736ce1..afb43f3e 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -81,41 +81,27 @@ struct DateRangeSelectorSheet: View { // Action buttons HStack(spacing: 16) { - Button(action: { + CustomButton( + title: t("wallet__filter_clear"), + variant: .secondary, + isDisabled: !hasSelection + ) { selectedStartDate = nil selectedEndDate = nil viewModel.clearDateRange() dismiss() - }) { - Text(t("wallet__filter_clear")) - .font(.system(size: 16, weight: .semibold)) - .frame(maxWidth: .infinity) - .frame(height: 50) - .foregroundColor(hasSelection ? .primary : .secondary) - .background( - RoundedRectangle(cornerRadius: 12) - .stroke(hasSelection ? Color.primary.opacity(0.3) : Color.secondary.opacity(0.2), lineWidth: 1) - ) } - .disabled(!hasSelection) .accessibilityIdentifier("CalendarClearButton") - Button(action: { + CustomButton( + title: t("wallet__filter_apply"), + variant: .primary, + isDisabled: !hasSelection + ) { viewModel.startDate = selectedStartDate viewModel.endDate = selectedEndDate dismiss() - }) { - Text(t("wallet__filter_apply")) - .font(.system(size: 16, weight: .semibold)) - .frame(maxWidth: .infinity) - .frame(height: 50) - .foregroundColor(.white) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(hasSelection ? Color.brandAccent : Color.gray.opacity(0.3)) - ) } - .disabled(!hasSelection) .accessibilityIdentifier("CalendarApplyButton") } .padding(.horizontal, 16) From d5c82fe9a0d52fb14201fcd87e9377e0041cc383 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 10:49:26 -0300 Subject: [PATCH 04/21] chore: remve date buttons --- .../Activity/DateRangeSelectorSheet.swift | 116 ++++++++---------- 1 file changed, 52 insertions(+), 64 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index afb43f3e..42f15086 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -1,13 +1,14 @@ import SwiftUI +// MARK: - DateRangeSelectorSheet + struct DateRangeSelectorSheet: View { @Environment(\.dismiss) private var dismiss @ObservedObject var viewModel: ActivityListViewModel @State private var selectedStartDate: Date? @State private var selectedEndDate: Date? - @State private var isSelectingStart = true init(viewModel: ActivityListViewModel) { self.viewModel = viewModel @@ -23,50 +24,31 @@ struct DateRangeSelectorSheet: View { VStack(spacing: 0) { // Date Range Picker VStack(alignment: .leading, spacing: 16) { - // Selection indicators - HStack(spacing: 12) { - DateSelectionButton( - title: t("wallet__filter_start_date"), - date: selectedStartDate, - isSelected: isSelectingStart, - action: { isSelectingStart = true } - ) - - DateSelectionButton( - title: t("wallet__filter_end_date"), - date: selectedEndDate, - isSelected: !isSelectingStart, - action: { isSelectingStart = false } - ) - } - .padding(.horizontal, 16) - .padding(.top, 20) - // Calendar DatePicker( "", selection: Binding( get: { - if isSelectingStart { - return selectedStartDate ?? Date() - } else { - return selectedEndDate ?? Date() - } + // Default to current start date, or today if none selected + return selectedStartDate ?? Date() }, set: { newDate in - if isSelectingStart { + if selectedStartDate == nil { + // First selection - set as start date selectedStartDate = newDate - if selectedEndDate == nil || newDate > selectedEndDate! { - selectedEndDate = newDate - } - isSelectingStart = false - } else { - if let start = selectedStartDate, newDate < start { - selectedEndDate = start + } else if selectedEndDate == nil { + // Second selection - set as end date + if newDate < selectedStartDate! { + // If new date is before start, swap them + selectedEndDate = selectedStartDate selectedStartDate = newDate } else { selectedEndDate = newDate } + } else { + // Both dates selected - reset and start over + selectedStartDate = newDate + selectedEndDate = nil } } ), @@ -75,6 +57,43 @@ struct DateRangeSelectorSheet: View { .datePickerStyle(.graphical) .tint(.brandAccent) .padding(.horizontal, 16) + .padding(.top, 20) + + // Display selected range + if let start = selectedStartDate { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(t("wallet__filter_start_date")) + .font(.system(size: 14)) + .foregroundColor(.white64) + Spacer() + Text(start.formatted(date: .abbreviated, time: .omitted)) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.textPrimary) + } + + if let end = selectedEndDate { + HStack { + Text(t("wallet__filter_end_date")) + .font(.system(size: 14)) + .foregroundColor(.white64) + Spacer() + Text(end.formatted(date: .abbreviated, time: .omitted)) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.textPrimary) + } + } else { + Text(t("wallet__filter_select_end_date")) + .font(.system(size: 14)) + .foregroundColor(.white32) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white08) + .cornerRadius(8) + .padding(.horizontal, 16) + } } Spacer() @@ -113,37 +132,6 @@ struct DateRangeSelectorSheet: View { } } -struct DateSelectionButton: View { - let title: String - let date: Date? - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.system(size: 12)) - .foregroundColor(.secondary) - - Text(date?.formatted(date: .abbreviated, time: .omitted) ?? t("wallet__filter_select_date")) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(date == nil ? .secondary : .primary) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(12) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(isSelected ? Color.brandAccent.opacity(0.1) : Color.clear) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(isSelected ? Color.brandAccent : Color.clear, lineWidth: 2) - ) - } - } -} - #Preview("Empty State") { DateRangeSelectorSheet(viewModel: ActivityListViewModel()) } From bedff43ab344bdca5a77cf0a151c7941a9a68965 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 10:57:04 -0300 Subject: [PATCH 05/21] chore: selected date component --- .../Activity/DateRangeSelectorSheet.swift | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 42f15086..16c44f07 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -61,35 +61,28 @@ struct DateRangeSelectorSheet: View { // Display selected range if let start = selectedStartDate { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(t("wallet__filter_start_date")) - .font(.system(size: 14)) - .foregroundColor(.white64) - Spacer() - Text(start.formatted(date: .abbreviated, time: .omitted)) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.textPrimary) - } - + HStack { if let end = selectedEndDate { - HStack { - Text(t("wallet__filter_end_date")) - .font(.system(size: 14)) - .foregroundColor(.white64) - Spacer() - Text(end.formatted(date: .abbreviated, time: .omitted)) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.textPrimary) - } + Text( + "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" + ) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.textPrimary) } else { + Text(start.formatted(.dateTime.month(.abbreviated).day().year())) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.textPrimary) + Text(" - ") + .font(.system(size: 14)) + .foregroundColor(.white32) Text(t("wallet__filter_select_end_date")) .font(.system(size: 14)) .foregroundColor(.white32) } } - .padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 12) + .padding(.horizontal, 16) .background(Color.white08) .cornerRadius(8) .padding(.horizontal, 16) From 27e43e9824bb0198cc886e7f838f7f8f6bbf059d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 11:27:20 -0300 Subject: [PATCH 06/21] chore: remove old component --- .../Activity/DateRangeSelector.swift | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 Bitkit/Components/Activity/DateRangeSelector.swift diff --git a/Bitkit/Components/Activity/DateRangeSelector.swift b/Bitkit/Components/Activity/DateRangeSelector.swift deleted file mode 100644 index d93198a4..00000000 --- a/Bitkit/Components/Activity/DateRangeSelector.swift +++ /dev/null @@ -1,109 +0,0 @@ -import SwiftUI - -struct DateRangeSelector: View { - @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel: ActivityListViewModel - @State private var startDate: Date - @State private var endDate: Date - - init(viewModel: ActivityListViewModel) { - self.viewModel = viewModel - // Initialize with current dates or default to today - _startDate = State(initialValue: viewModel.startDate ?? Calendar.current.startOfDay(for: Date())) - _endDate = State(initialValue: viewModel.endDate ?? Date()) - } - - private func setDateRange(daysBack: Int) { - let today = Date() - let calendar = Calendar.current - - // Set end date to today - endDate = today - - // Set start date to X days back at start of day - if let daysBackDate = calendar.date(byAdding: .day, value: -daysBack, to: today) { - startDate = calendar.startOfDay(for: daysBackDate) - } - } - - var body: some View { - NavigationStack { - VStack { - Form { - Section { - DatePicker("Start Date", selection: $startDate, displayedComponents: [.date]) - .onChange(of: startDate) { newValue in - viewModel.startDate = newValue - } - DatePicker("End Date", selection: $endDate, displayedComponents: [.date]) - .onChange(of: endDate) { newValue in - viewModel.endDate = newValue - } - } - - Section { - Button("Today") { - setDateRange(daysBack: 0) - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() - } - Button("Last 7 days") { - setDateRange(daysBack: 7) - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() - } - Button("Last 30 days") { - setDateRange(daysBack: 30) - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() - } - Button("Last 90 days") { - setDateRange(daysBack: 90) - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() - } - Button("This year") { - let calendar = Calendar.current - startDate = calendar.date(from: calendar.dateComponents([.year], from: Date())) ?? Date() - endDate = Date() - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() - } - } header: { - Text("Quick Select") - } - } - - Spacer() - - HStack { - Spacer() - Button("Clear") { - viewModel.clearDateRange() - dismiss() - } - Spacer() - Button("Done") { - dismiss() - } - Spacer() - } - .padding() - } - .navigationTitle("Date Range") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Close") { - dismiss() - } - } - } - } - } -} From 30c4d125decfc7f01da4b0cead5f09585d58a164 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 7 Oct 2025 12:47:45 -0300 Subject: [PATCH 07/21] chore: replace DatePicker with MultiDatePicker --- .../Activity/DateRangeSelectorSheet.swift | 162 +++++++++++++----- 1 file changed, 117 insertions(+), 45 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 16c44f07..d0b31199 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -1,23 +1,89 @@ - import SwiftUI // MARK: - DateRangeSelectorSheet struct DateRangeSelectorSheet: View { @Environment(\.dismiss) private var dismiss + @Environment(\.calendar) var calendar @ObservedObject var viewModel: ActivityListViewModel - @State private var selectedStartDate: Date? - @State private var selectedEndDate: Date? + @State private var selectedDates: Set = [] + @State private var startDate: Date? + @State private var endDate: Date? + + let datePickerComponents: Set = [.calendar, .era, .year, .month, .day] init(viewModel: ActivityListViewModel) { self.viewModel = viewModel - _selectedStartDate = State(initialValue: viewModel.startDate) - _selectedEndDate = State(initialValue: viewModel.endDate) + + // Initialize with current date range if exists + var initialDates: Set = [] + if let start = viewModel.startDate, let end = viewModel.endDate { + let calendar = Calendar.current + var currentDate = calendar.startOfDay(for: start) + let endOfDay = calendar.startOfDay(for: end) + + while currentDate <= endOfDay { + if let components = calendar.dateComponents(datePickerComponents, from: currentDate) as DateComponents? { + initialDates.insert(components) + } + currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate + } + } + + _selectedDates = State(initialValue: initialDates) + _startDate = State(initialValue: viewModel.startDate) + _endDate = State(initialValue: viewModel.endDate) } private var hasSelection: Bool { - selectedStartDate != nil && selectedEndDate != nil + startDate != nil && endDate != nil + } + + private var datesBinding: Binding> { + Binding { + selectedDates + } set: { newValue in + if newValue.isEmpty { + selectedDates = newValue + startDate = nil + endDate = nil + } else if newValue.count > selectedDates.count { + // Date was added + if newValue.count == 1 { + // First date selected - set as start + selectedDates = newValue + if let components = newValue.first, + let date = calendar.date(from: components) + { + startDate = date + endDate = nil + } + } else if newValue.count == 2 { + // Second date selected - fill the range + selectedDates = filledRange(selectedDates: newValue) + updateStartEndDates() + } else if let firstMissingDate = newValue.subtracting(selectedDates).first { + // Additional date tapped - start new range + selectedDates = [firstMissingDate] + if let date = calendar.date(from: firstMissingDate) { + startDate = date + endDate = nil + } + } + } else if let firstMissingDate = selectedDates.subtracting(newValue).first { + // Date was removed - start new range from this date + selectedDates = [firstMissingDate] + if let date = calendar.date(from: firstMissingDate) { + startDate = date + endDate = nil + } + } else { + selectedDates = [] + startDate = nil + endDate = nil + } + } } var body: some View { @@ -25,44 +91,16 @@ struct DateRangeSelectorSheet: View { // Date Range Picker VStack(alignment: .leading, spacing: 16) { // Calendar - DatePicker( - "", - selection: Binding( - get: { - // Default to current start date, or today if none selected - return selectedStartDate ?? Date() - }, - set: { newDate in - if selectedStartDate == nil { - // First selection - set as start date - selectedStartDate = newDate - } else if selectedEndDate == nil { - // Second selection - set as end date - if newDate < selectedStartDate! { - // If new date is before start, swap them - selectedEndDate = selectedStartDate - selectedStartDate = newDate - } else { - selectedEndDate = newDate - } - } else { - // Both dates selected - reset and start over - selectedStartDate = newDate - selectedEndDate = nil - } - } - ), - displayedComponents: .date - ) - .datePickerStyle(.graphical) - .tint(.brandAccent) - .padding(.horizontal, 16) - .padding(.top, 20) + MultiDatePicker("", selection: datesBinding) + .datePickerStyle(.graphical) + .tint(.brandAccent) + .padding(.horizontal, 16) + .padding(.top, 20) // Display selected range - if let start = selectedStartDate { + if let start = startDate { HStack { - if let end = selectedEndDate { + if let end = endDate { Text( "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" ) @@ -98,8 +136,9 @@ struct DateRangeSelectorSheet: View { variant: .secondary, isDisabled: !hasSelection ) { - selectedStartDate = nil - selectedEndDate = nil + selectedDates = [] + startDate = nil + endDate = nil viewModel.clearDateRange() dismiss() } @@ -110,8 +149,8 @@ struct DateRangeSelectorSheet: View { variant: .primary, isDisabled: !hasSelection ) { - viewModel.startDate = selectedStartDate - viewModel.endDate = selectedEndDate + viewModel.startDate = startDate + viewModel.endDate = endDate dismiss() } .accessibilityIdentifier("CalendarApplyButton") @@ -123,6 +162,39 @@ struct DateRangeSelectorSheet: View { .presentationDetents([.height(600)]) .presentationDragIndicator(.visible) } + + // MARK: - Helper Methods + + private func filledRange(selectedDates: Set) -> Set { + let allDates = selectedDates.compactMap { calendar.date(from: $0) } + guard allDates.count == 2, + let startDate = allDates.min(), + let endDate = allDates.max() + else { + return selectedDates + } + + var dateRange: Set = [] + var currentDate = startDate + + while currentDate <= endDate { + if let components = calendar.dateComponents(datePickerComponents, from: currentDate) as DateComponents? { + dateRange.insert(components) + } + guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { + break + } + currentDate = nextDate + } + + return dateRange + } + + private func updateStartEndDates() { + let allDates = selectedDates.compactMap { calendar.date(from: $0) } + startDate = allDates.min() + endDate = allDates.max() + } } #Preview("Empty State") { From 14cfd80cc44daf93fbaa3104e0606000fd3f0b1f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Oct 2025 09:58:48 -0300 Subject: [PATCH 08/21] fix: selected date text --- .../Components/Activity/DateRangeSelectorSheet.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index d0b31199..7305c194 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -104,25 +104,17 @@ struct DateRangeSelectorSheet: View { Text( "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" ) - .font(.system(size: 14, weight: .medium)) + .font(.system(size: 17, weight: .medium)) .foregroundColor(.textPrimary) } else { Text(start.formatted(.dateTime.month(.abbreviated).day().year())) .font(.system(size: 14, weight: .medium)) .foregroundColor(.textPrimary) - Text(" - ") - .font(.system(size: 14)) - .foregroundColor(.white32) - Text(t("wallet__filter_select_end_date")) - .font(.system(size: 14)) - .foregroundColor(.white32) } } .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 12) + .padding(.bottom, 36) .padding(.horizontal, 16) - .background(Color.white08) - .cornerRadius(8) .padding(.horizontal, 16) } } From ded8408093032d509ed1d577d726dd99086f07f8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Oct 2025 10:08:25 -0300 Subject: [PATCH 09/21] feat: selected date text style --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 7305c194..01bd1a07 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -101,21 +101,16 @@ struct DateRangeSelectorSheet: View { if let start = startDate { HStack { if let end = endDate { - Text( + BodyMSBText( "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" ) - .font(.system(size: 17, weight: .medium)) - .foregroundColor(.textPrimary) } else { - Text(start.formatted(.dateTime.month(.abbreviated).day().year())) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.textPrimary) + BodyMSBText(start.formatted(.dateTime.month(.abbreviated).day().year())) } } .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, 36) .padding(.horizontal, 16) - .padding(.horizontal, 16) } } From d1f44a1a0ab5594ac1cae9bdea5f9fddaca337ac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Oct 2025 10:35:54 -0300 Subject: [PATCH 10/21] chore: padding --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 01bd1a07..49ce63e7 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -88,6 +88,9 @@ struct DateRangeSelectorSheet: View { var body: some View { VStack(spacing: 0) { + BodyMBoldText(t("wallet__filter_title"), textColor: .white) + .padding(.top, 32) + // Date Range Picker VStack(alignment: .leading, spacing: 16) { // Calendar @@ -95,7 +98,7 @@ struct DateRangeSelectorSheet: View { .datePickerStyle(.graphical) .tint(.brandAccent) .padding(.horizontal, 16) - .padding(.top, 20) + .padding(.top, 26) // Display selected range if let start = startDate { From e93e1e9c64882d312541219a662e211c1f1803f4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Oct 2025 11:47:17 -0300 Subject: [PATCH 11/21] chore: sheet wrapper --- .../Activity/ActivityListFilter.swift | 2 +- .../Activity/DateRangeSelectorSheet.swift | 121 +++++++++--------- Bitkit/ViewModels/SheetViewModel.swift | 1 + 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Bitkit/Components/Activity/ActivityListFilter.swift b/Bitkit/Components/Activity/ActivityListFilter.swift index 1a5dbacd..df98981a 100644 --- a/Bitkit/Components/Activity/ActivityListFilter.swift +++ b/Bitkit/Components/Activity/ActivityListFilter.swift @@ -51,7 +51,7 @@ struct ActivityListFilter: View { .background(Color.gray6) .cornerRadius(32) .sheet(isPresented: $showingDateRange) { - DateRangeSelectorSheet(viewModel: viewModel) + DateRangeSelectorSheet(viewModel: viewModel, isPresented: $showingDateRange) } .sheet(isPresented: $showingTagSelector) { TagFilterSheet(viewModel: viewModel, isPresented: $showingTagSelector) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 49ce63e7..2c089572 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -3,9 +3,9 @@ import SwiftUI // MARK: - DateRangeSelectorSheet struct DateRangeSelectorSheet: View { - @Environment(\.dismiss) private var dismiss @Environment(\.calendar) var calendar @ObservedObject var viewModel: ActivityListViewModel + @Binding var isPresented: Bool @State private var selectedDates: Set = [] @State private var startDate: Date? @@ -13,8 +13,9 @@ struct DateRangeSelectorSheet: View { let datePickerComponents: Set = [.calendar, .era, .year, .month, .day] - init(viewModel: ActivityListViewModel) { + init(viewModel: ActivityListViewModel, isPresented: Binding) { self.viewModel = viewModel + _isPresented = isPresented // Initialize with current date range if exists var initialDates: Set = [] @@ -87,70 +88,68 @@ struct DateRangeSelectorSheet: View { } var body: some View { - VStack(spacing: 0) { - BodyMBoldText(t("wallet__filter_title"), textColor: .white) - .padding(.top, 32) - - // Date Range Picker - VStack(alignment: .leading, spacing: 16) { - // Calendar - MultiDatePicker("", selection: datesBinding) - .datePickerStyle(.graphical) - .tint(.brandAccent) - .padding(.horizontal, 16) - .padding(.top, 26) - - // Display selected range - if let start = startDate { - HStack { - if let end = endDate { - BodyMSBText( - "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" - ) - } else { - BodyMSBText(start.formatted(.dateTime.month(.abbreviated).day().year())) + Sheet(id: .dateRangeSelector, data: nil) { + VStack(spacing: 0) { + BodyMBoldText(t("wallet__filter_title"), textColor: .white) + .padding(.top, 32) + + // Date Range Picker + VStack(alignment: .leading, spacing: 16) { + // Calendar + MultiDatePicker("", selection: datesBinding) + .datePickerStyle(.graphical) + .tint(.brandAccent) + .padding(.horizontal, 16) + .padding(.top, 26) + + // Display selected range + if let start = startDate { + HStack { + if let end = endDate { + BodyMSBText( + "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" + ) + } else { + BodyMSBText(start.formatted(.dateTime.month(.abbreviated).day().year())) + } } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 36) + .padding(.horizontal, 16) } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.bottom, 36) - .padding(.horizontal, 16) } - } - Spacer() - - // Action buttons - HStack(spacing: 16) { - CustomButton( - title: t("wallet__filter_clear"), - variant: .secondary, - isDisabled: !hasSelection - ) { - selectedDates = [] - startDate = nil - endDate = nil - viewModel.clearDateRange() - dismiss() - } - .accessibilityIdentifier("CalendarClearButton") - - CustomButton( - title: t("wallet__filter_apply"), - variant: .primary, - isDisabled: !hasSelection - ) { - viewModel.startDate = startDate - viewModel.endDate = endDate - dismiss() + Spacer() + + // Action buttons + HStack(spacing: 16) { + CustomButton( + title: t("wallet__filter_clear"), + variant: .secondary, + isDisabled: !hasSelection + ) { + selectedDates = [] + startDate = nil + endDate = nil + viewModel.clearDateRange() + } + .accessibilityIdentifier("CalendarClearButton") + + CustomButton( + title: t("wallet__filter_apply"), + variant: .primary, + isDisabled: !hasSelection + ) { + viewModel.startDate = startDate + viewModel.endDate = endDate + isPresented = false + } + .accessibilityIdentifier("CalendarApplyButton") } - .accessibilityIdentifier("CalendarApplyButton") + .padding(.horizontal, 16) + .padding(.bottom, 16) } - .padding(.horizontal, 16) - .padding(.bottom, 16) } - .sheetBackground() - .presentationDetents([.height(600)]) - .presentationDragIndicator(.visible) } // MARK: - Helper Methods @@ -188,12 +187,12 @@ struct DateRangeSelectorSheet: View { } #Preview("Empty State") { - DateRangeSelectorSheet(viewModel: ActivityListViewModel()) + DateRangeSelectorSheet(viewModel: ActivityListViewModel(), isPresented: .constant(true)) } #Preview("With Selection") { let viewModel = ActivityListViewModel() viewModel.startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) viewModel.endDate = Date() - return DateRangeSelectorSheet(viewModel: viewModel) + return DateRangeSelectorSheet(viewModel: viewModel, isPresented: .constant(true)) } diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index 4bb1ef32..1be7b927 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -17,6 +17,7 @@ enum SheetID: String, CaseIterable { case security case send case tagFilter + case dateRangeSelector } struct SheetConfiguration { From 65db24bdef5a848b3c9ded9d41ab1b899f093b7b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Oct 2025 12:01:23 -0300 Subject: [PATCH 12/21] fix: date formmating --- .../Activity/DateRangeSelectorSheet.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 2c089572..0d128e32 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -107,15 +107,23 @@ struct DateRangeSelectorSheet: View { HStack { if let end = endDate { BodyMSBText( - "\(start.formatted(.dateTime.month(.abbreviated).day().year())) - \(end.formatted(.dateTime.month(.abbreviated).day().year()))" + "\(formatDate(start)) - \(formatDate(end))" ) + .id("\(formatDate(start))-\(formatDate(end))") } else { - BodyMSBText(start.formatted(.dateTime.month(.abbreviated).day().year())) + BodyMSBText(formatDate(start)) + .id(formatDate(start)) } } .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, 36) .padding(.horizontal, 16) + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity), + removal: .scale.combined(with: .opacity) + )) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: startDate) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: endDate) } } @@ -154,6 +162,12 @@ struct DateRangeSelectorSheet: View { // MARK: - Helper Methods + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MMM d, yyyy" + return formatter.string(from: date) + } + private func filledRange(selectedDates: Set) -> Set { let allDates = selectedDates.compactMap { calendar.date(from: $0) } guard allDates.count == 2, From 5965f7626a311e804a662073a88c7d68a2ba1613 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 08:16:33 -0300 Subject: [PATCH 13/21] fix: layout jump, single date selection, sheet header --- .../Activity/DateRangeSelectorSheet.swift | 97 ++++++++++++------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 0d128e32..282662f3 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -1,8 +1,12 @@ import SwiftUI -// MARK: - DateRangeSelectorSheet +struct DateRangeSelectorSheetItem: SheetItem { + let id: SheetID = .dateRangeSelector + let size: SheetSize = .medium +} struct DateRangeSelectorSheet: View { + @EnvironmentObject private var sheets: SheetViewModel @Environment(\.calendar) var calendar @ObservedObject var viewModel: ActivityListViewModel @Binding var isPresented: Bool @@ -10,6 +14,7 @@ struct DateRangeSelectorSheet: View { @State private var selectedDates: Set = [] @State private var startDate: Date? @State private var endDate: Date? + @State private var pickerKey: UUID = .init() let datePickerComponents: Set = [.calendar, .era, .year, .month, .day] @@ -17,10 +22,14 @@ struct DateRangeSelectorSheet: View { self.viewModel = viewModel _isPresented = isPresented + let calendar = Calendar.current + // Initialize with current date range if exists var initialDates: Set = [] + var initialStart: Date? + var initialEnd: Date? + if let start = viewModel.startDate, let end = viewModel.endDate { - let calendar = Calendar.current var currentDate = calendar.startOfDay(for: start) let endOfDay = calendar.startOfDay(for: end) @@ -30,15 +39,18 @@ struct DateRangeSelectorSheet: View { } currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate } + + initialStart = start + initialEnd = end } _selectedDates = State(initialValue: initialDates) - _startDate = State(initialValue: viewModel.startDate) - _endDate = State(initialValue: viewModel.endDate) + _startDate = State(initialValue: initialStart) + _endDate = State(initialValue: initialEnd) } private var hasSelection: Bool { - startDate != nil && endDate != nil + startDate != nil } private var datesBinding: Binding> { @@ -52,15 +64,15 @@ struct DateRangeSelectorSheet: View { } else if newValue.count > selectedDates.count { // Date was added if newValue.count == 1 { - // First date selected - set as start + // First date selected - set as start (single day selection) selectedDates = newValue if let components = newValue.first, let date = calendar.date(from: components) { startDate = date - endDate = nil + endDate = date } - } else if newValue.count == 2 { + } else if selectedDates.count == 1 { // Second date selected - fill the range selectedDates = filledRange(selectedDates: newValue) updateStartEndDates() @@ -69,7 +81,7 @@ struct DateRangeSelectorSheet: View { selectedDates = [firstMissingDate] if let date = calendar.date(from: firstMissingDate) { startDate = date - endDate = nil + endDate = date } } } else if let firstMissingDate = selectedDates.subtracting(newValue).first { @@ -77,7 +89,7 @@ struct DateRangeSelectorSheet: View { selectedDates = [firstMissingDate] if let date = calendar.date(from: firstMissingDate) { startDate = date - endDate = nil + endDate = date } } else { selectedDates = [] @@ -88,43 +100,55 @@ struct DateRangeSelectorSheet: View { } var body: some View { - Sheet(id: .dateRangeSelector, data: nil) { + Sheet(id: .dateRangeSelector, data: DateRangeSelectorSheetItem()) { VStack(spacing: 0) { - BodyMBoldText(t("wallet__filter_title"), textColor: .white) - .padding(.top, 32) + SheetHeader(title: t("wallet__filter_title")) // Date Range Picker VStack(alignment: .leading, spacing: 16) { - // Calendar + // Calendar - recreate with new UUID to show selected month MultiDatePicker("", selection: datesBinding) .datePickerStyle(.graphical) .tint(.brandAccent) .padding(.horizontal, 16) - .padding(.top, 26) - - // Display selected range - if let start = startDate { - HStack { - if let end = endDate { - BodyMSBText( - "\(formatDate(start)) - \(formatDate(end))" - ) - .id("\(formatDate(start))-\(formatDate(end))") - } else { - BodyMSBText(formatDate(start)) - .id(formatDate(start)) + .id(pickerKey) + .onAppear { + // Force picker recreation when sheet appears to show selected month + if viewModel.startDate != nil { + pickerKey = UUID() } } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.bottom, 36) - .padding(.horizontal, 16) - .transition(.asymmetric( - insertion: .scale.combined(with: .opacity), - removal: .scale.combined(with: .opacity) - )) - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: startDate) - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: endDate) + + // Display selected range (fixed height to prevent layout jump) + VStack { + if let start = startDate { + HStack { + if let end = endDate, start != end { + BodyMSBText( + "\(formatDate(start)) - \(formatDate(end))" + ) + .id("\(formatDate(start))-\(formatDate(end))") + } else { + BodyMSBText(formatDate(start)) + .id(formatDate(start)) + } + } + .frame(maxWidth: .infinity, alignment: .center) + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity), + removal: .scale.combined(with: .opacity) + )) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: startDate) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: endDate) + } else { + // Placeholder to maintain height + BodyMSBText(" ") + .opacity(0) + } } + .frame(height: 24) + .padding(.bottom, 36) + .padding(.horizontal, 16) } Spacer() @@ -155,7 +179,6 @@ struct DateRangeSelectorSheet: View { .accessibilityIdentifier("CalendarApplyButton") } .padding(.horizontal, 16) - .padding(.bottom, 16) } } } From c098033a25d3e9075cfaa52c73fff5845332f457 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 08:34:48 -0300 Subject: [PATCH 14/21] fix: implement a custom calendar component --- .../Activity/DateRangeSelectorSheet.swift | 300 ++++++++++++------ 1 file changed, 196 insertions(+), 104 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 282662f3..17865efc 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -1,102 +1,69 @@ import SwiftUI +// MARK: - DateRangeSelectorSheetItem + struct DateRangeSelectorSheetItem: SheetItem { let id: SheetID = .dateRangeSelector let size: SheetSize = .medium } +// MARK: - DateRangeSelectorSheet + struct DateRangeSelectorSheet: View { @EnvironmentObject private var sheets: SheetViewModel @Environment(\.calendar) var calendar @ObservedObject var viewModel: ActivityListViewModel @Binding var isPresented: Bool - @State private var selectedDates: Set = [] + @State private var displayedMonth: Date @State private var startDate: Date? @State private var endDate: Date? - @State private var pickerKey: UUID = .init() - - let datePickerComponents: Set = [.calendar, .era, .year, .month, .day] init(viewModel: ActivityListViewModel, isPresented: Binding) { self.viewModel = viewModel _isPresented = isPresented - let calendar = Calendar.current - - // Initialize with current date range if exists - var initialDates: Set = [] - var initialStart: Date? - var initialEnd: Date? - - if let start = viewModel.startDate, let end = viewModel.endDate { - var currentDate = calendar.startOfDay(for: start) - let endOfDay = calendar.startOfDay(for: end) - - while currentDate <= endOfDay { - if let components = calendar.dateComponents(datePickerComponents, from: currentDate) as DateComponents? { - initialDates.insert(components) - } - currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate - } - - initialStart = start - initialEnd = end - } - - _selectedDates = State(initialValue: initialDates) - _startDate = State(initialValue: initialStart) - _endDate = State(initialValue: initialEnd) + // Initialize displayed month to the selected start date or current date + let initialMonth = viewModel.startDate ?? Date() + _displayedMonth = State(initialValue: initialMonth) + _startDate = State(initialValue: viewModel.startDate) + _endDate = State(initialValue: viewModel.endDate) } private var hasSelection: Bool { startDate != nil } - private var datesBinding: Binding> { - Binding { - selectedDates - } set: { newValue in - if newValue.isEmpty { - selectedDates = newValue - startDate = nil - endDate = nil - } else if newValue.count > selectedDates.count { - // Date was added - if newValue.count == 1 { - // First date selected - set as start (single day selection) - selectedDates = newValue - if let components = newValue.first, - let date = calendar.date(from: components) - { - startDate = date - endDate = date - } - } else if selectedDates.count == 1 { - // Second date selected - fill the range - selectedDates = filledRange(selectedDates: newValue) - updateStartEndDates() - } else if let firstMissingDate = newValue.subtracting(selectedDates).first { - // Additional date tapped - start new range - selectedDates = [firstMissingDate] - if let date = calendar.date(from: firstMissingDate) { - startDate = date - endDate = date - } - } - } else if let firstMissingDate = selectedDates.subtracting(newValue).first { - // Date was removed - start new range from this date - selectedDates = [firstMissingDate] - if let date = calendar.date(from: firstMissingDate) { - startDate = date - endDate = date - } + private var monthYearString: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM yyyy" + return formatter.string(from: displayedMonth) + } + + private var daysInMonth: [Date?] { + guard let monthInterval = calendar.dateInterval(of: .month, for: displayedMonth), + let monthFirstWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start) + else { + return [] + } + + var days: [Date?] = [] + var currentDate = monthFirstWeek.start + + while days.count < 42 { // 6 weeks max + if calendar.isDate(currentDate, equalTo: displayedMonth, toGranularity: .month) { + days.append(currentDate) } else { - selectedDates = [] - startDate = nil - endDate = nil + days.append(nil) + } + + guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { + break } + currentDate = nextDate } + + return days } var body: some View { @@ -104,20 +71,65 @@ struct DateRangeSelectorSheet: View { VStack(spacing: 0) { SheetHeader(title: t("wallet__filter_title")) - // Date Range Picker VStack(alignment: .leading, spacing: 16) { - // Calendar - recreate with new UUID to show selected month - MultiDatePicker("", selection: datesBinding) - .datePickerStyle(.graphical) - .tint(.brandAccent) - .padding(.horizontal, 16) - .id(pickerKey) - .onAppear { - // Force picker recreation when sheet appears to show selected month - if viewModel.startDate != nil { - pickerKey = UUID() + // Month navigation + HStack { + Button(action: previousMonth) { + Image(systemName: "chevron.left") + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + .accessibilityIdentifier("PrevMonth") + + Spacer() + + Text(monthYearString) + .font(.custom(Fonts.semiBold, size: 17)) + .foregroundColor(.white) + + Spacer() + + Button(action: nextMonth) { + Image(systemName: "chevron.right") + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + .accessibilityIdentifier("NextMonth") + } + .padding(.horizontal, 16) + + // Weekday headers + HStack(spacing: 0) { + ForEach(calendar.veryShortWeekdaySymbols, id: \.self) { symbol in + Text(symbol) + .font(.custom(Fonts.regular, size: 12)) + .foregroundColor(.white64) + .frame(maxWidth: .infinity) + } + } + .padding(.horizontal, 16) + + // Calendar grid + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0), count: 7), spacing: 8) { + ForEach(Array(daysInMonth.enumerated()), id: \.offset) { _, date in + if let date { + CalendarDayView( + date: date, + isSelected: isDateInRange(date), + isStartDate: calendar.isDate(date, inSameDayAs: startDate ?? Date.distantPast), + isEndDate: calendar.isDate(date, inSameDayAs: endDate ?? Date.distantPast), + isToday: calendar.isDateInToday(date) + ) { + selectDate(date) + } + .accessibilityIdentifier(calendar.isDateInToday(date) ? "Today" : "Day-\(calendar.component(.day, from: date))") + } else { + Color.clear + .frame(height: 40) } } + } + .padding(.horizontal, 16) // Display selected range (fixed height to prevent layout jump) VStack { @@ -160,10 +172,10 @@ struct DateRangeSelectorSheet: View { variant: .secondary, isDisabled: !hasSelection ) { - selectedDates = [] startDate = nil endDate = nil viewModel.clearDateRange() + isPresented = false } .accessibilityIdentifier("CalendarClearButton") @@ -191,35 +203,115 @@ struct DateRangeSelectorSheet: View { return formatter.string(from: date) } - private func filledRange(selectedDates: Set) -> Set { - let allDates = selectedDates.compactMap { calendar.date(from: $0) } - guard allDates.count == 2, - let startDate = allDates.min(), - let endDate = allDates.max() - else { - return selectedDates - } + private func isDateInRange(_ date: Date) -> Bool { + guard let start = startDate else { return false } + let end = endDate ?? start - var dateRange: Set = [] - var currentDate = startDate + let normalizedDate = calendar.startOfDay(for: date) + let normalizedStart = calendar.startOfDay(for: start) + let normalizedEnd = calendar.startOfDay(for: end) - while currentDate <= endDate { - if let components = calendar.dateComponents(datePickerComponents, from: currentDate) as DateComponents? { - dateRange.insert(components) - } - guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { - break + return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd + } + + private func selectDate(_ date: Date) { + let normalizedDate = calendar.startOfDay(for: date) + + if startDate == nil { + // First selection + startDate = normalizedDate + endDate = normalizedDate + } else if let start = startDate, let end = endDate, start == end { + // Second selection - create range + let normalizedStart = calendar.startOfDay(for: start) + if normalizedDate < normalizedStart { + startDate = normalizedDate + endDate = normalizedStart + } else if normalizedDate == normalizedStart { + // Same date clicked - do nothing or clear + return + } else { + endDate = normalizedDate } - currentDate = nextDate + } else { + // Third selection - start new range + startDate = normalizedDate + endDate = normalizedDate + } + } + + private func previousMonth() { + if let newMonth = calendar.date(byAdding: .month, value: -1, to: displayedMonth) { + displayedMonth = newMonth } + } + + private func nextMonth() { + if let newMonth = calendar.date(byAdding: .month, value: 1, to: displayedMonth) { + displayedMonth = newMonth + } + } +} - return dateRange +// MARK: - CalendarDayView + +struct CalendarDayView: View { + let date: Date + let isSelected: Bool + let isStartDate: Bool + let isEndDate: Bool + let isToday: Bool + let action: () -> Void + + @Environment(\.calendar) var calendar + + private var dayNumber: String { + String(calendar.component(.day, from: date)) } - private func updateStartEndDates() { - let allDates = selectedDates.compactMap { calendar.date(from: $0) } - startDate = allDates.min() - endDate = allDates.max() + var body: some View { + Button(action: action) { + ZStack { + // Selection background + if isSelected { + if isStartDate && isEndDate { + // Single day or start=end + Circle() + .fill(Color.brandAccent) + } else if isStartDate { + HStack(spacing: 0) { + Circle() + .fill(Color.brandAccent) + Rectangle() + .fill(Color.brandAccent.opacity(0.3)) + } + } else if isEndDate { + HStack(spacing: 0) { + Rectangle() + .fill(Color.brandAccent.opacity(0.3)) + Circle() + .fill(Color.brandAccent) + } + } else { + // Middle of range + Rectangle() + .fill(Color.brandAccent.opacity(0.3)) + } + } + + // Day number + Text(dayNumber) + .font(.custom(Fonts.regular, size: 16)) + .foregroundColor(isSelected ? .black : .white) + + // Today indicator + if isToday && !isSelected { + Circle() + .stroke(Color.brandAccent, lineWidth: 1) + } + } + } + .frame(height: 40) } } From f73b1b2d266fcd422b90603a2c640db953fa5737 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 08:51:56 -0300 Subject: [PATCH 15/21] fix: selected day background --- .../Activity/DateRangeSelectorSheet.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 17865efc..b6266558 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -279,19 +279,11 @@ struct CalendarDayView: View { Circle() .fill(Color.brandAccent) } else if isStartDate { - HStack(spacing: 0) { - Circle() - .fill(Color.brandAccent) - Rectangle() - .fill(Color.brandAccent.opacity(0.3)) - } + Circle() + .fill(Color.brandAccent) } else if isEndDate { - HStack(spacing: 0) { - Rectangle() - .fill(Color.brandAccent.opacity(0.3)) - Circle() - .fill(Color.brandAccent) - } + Circle() + .fill(Color.brandAccent) } else { // Middle of range Rectangle() From 7faaa278efcfbbeee0828f235327f2c9e07bf200 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 08:58:44 -0300 Subject: [PATCH 16/21] fix: colors --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index b6266558..afae3fbf 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -277,13 +277,13 @@ struct CalendarDayView: View { if isStartDate && isEndDate { // Single day or start=end Circle() - .fill(Color.brandAccent) + .fill(Color.brand16) } else if isStartDate { Circle() - .fill(Color.brandAccent) + .fill(Color.brand16) } else if isEndDate { Circle() - .fill(Color.brandAccent) + .fill(Color.brand16) } else { // Middle of range Rectangle() @@ -294,7 +294,7 @@ struct CalendarDayView: View { // Day number Text(dayNumber) .font(.custom(Fonts.regular, size: 16)) - .foregroundColor(isSelected ? .black : .white) + .foregroundColor(.white) // Today indicator if isToday && !isSelected { From 37697b40ba49f8614d0d5e9f3716223bc49f1be8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 09:47:14 -0300 Subject: [PATCH 17/21] fix: week day style --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index afae3fbf..fcac41cd 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -100,9 +100,8 @@ struct DateRangeSelectorSheet: View { // Weekday headers HStack(spacing: 0) { - ForEach(calendar.veryShortWeekdaySymbols, id: \.self) { symbol in - Text(symbol) - .font(.custom(Fonts.regular, size: 12)) + ForEach(calendar.shortWeekdaySymbols, id: \.self) { symbol in + CaptionText(symbol.uppercased()) .foregroundColor(.white64) .frame(maxWidth: .infinity) } From bbd719a5081059d8ef13b526b9a7574d007627c1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 10:18:26 -0300 Subject: [PATCH 18/21] fix: chevron style --- .../Activity/DateRangeSelectorSheet.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index fcac41cd..2900b66f 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -74,24 +74,22 @@ struct DateRangeSelectorSheet: View { VStack(alignment: .leading, spacing: 16) { // Month navigation HStack { + Text(monthYearString) + .font(.custom(Fonts.semiBold, size: 17)) + .foregroundColor(.white) + Spacer() + Button(action: previousMonth) { Image(systemName: "chevron.left") - .foregroundColor(.white) + .foregroundColor(.brandAccent) .frame(width: 44, height: 44) } + .padding(.leading, 8) .accessibilityIdentifier("PrevMonth") - Spacer() - - Text(monthYearString) - .font(.custom(Fonts.semiBold, size: 17)) - .foregroundColor(.white) - - Spacer() - Button(action: nextMonth) { Image(systemName: "chevron.right") - .foregroundColor(.white) + .foregroundColor(.brandAccent) .frame(width: 44, height: 44) } .accessibilityIdentifier("NextMonth") @@ -293,7 +291,7 @@ struct CalendarDayView: View { // Day number Text(dayNumber) .font(.custom(Fonts.regular, size: 16)) - .foregroundColor(.white) + .foregroundColor(isStartDate || isEndDate ? Color.brandAccent : Color.white) // Today indicator if isToday && !isSelected { From d562d19cb91f0d07a0b50f22e98eec10fcda6359 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 10:24:50 -0300 Subject: [PATCH 19/21] fix: middle day background color --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 2900b66f..057facc3 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -284,7 +284,7 @@ struct CalendarDayView: View { } else { // Middle of range Rectangle() - .fill(Color.brandAccent.opacity(0.3)) + .fill(Color.brand16) } } From 95605cfaa461c993c4a3ecc66cca2e9ec87d6857 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Fri, 10 Oct 2025 16:35:50 +0200 Subject: [PATCH 20/21] fix(activity): fix weekday headers, polishing --- .../Activity/DateRangeSelectorSheet.swift | 72 ++++++++++++------- Bitkit/Views/Sheets/Sheet.swift | 12 +++- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 057facc3..21a67545 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -4,13 +4,12 @@ import SwiftUI struct DateRangeSelectorSheetItem: SheetItem { let id: SheetID = .dateRangeSelector - let size: SheetSize = .medium + let size: SheetSize = .calendar } // MARK: - DateRangeSelectorSheet struct DateRangeSelectorSheet: View { - @EnvironmentObject private var sheets: SheetViewModel @Environment(\.calendar) var calendar @ObservedObject var viewModel: ActivityListViewModel @Binding var isPresented: Bool @@ -41,14 +40,25 @@ struct DateRangeSelectorSheet: View { } private var daysInMonth: [Date?] { - guard let monthInterval = calendar.dateInterval(of: .month, for: displayedMonth), - let monthFirstWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start) - else { + guard let monthInterval = calendar.dateInterval(of: .month, for: displayedMonth) else { + return [] + } + + // Get the first day of the month + let firstDayOfMonth = monthInterval.start + + // Find the first day of the week that contains the first day of the month + // This ensures alignment with the weekday headers + let firstWeekday = calendar.component(.weekday, from: firstDayOfMonth) + let firstDayOfWeek = calendar.firstWeekday + let daysToSubtract = (firstWeekday - firstDayOfWeek + 7) % 7 + + guard let calendarStartDate = calendar.date(byAdding: .day, value: -daysToSubtract, to: firstDayOfMonth) else { return [] } var days: [Date?] = [] - var currentDate = monthFirstWeek.start + var currentDate = calendarStartDate while days.count < 42 { // 6 weeks max if calendar.isDate(currentDate, equalTo: displayedMonth, toGranularity: .month) { @@ -74,23 +84,26 @@ struct DateRangeSelectorSheet: View { VStack(alignment: .leading, spacing: 16) { // Month navigation HStack { - Text(monthYearString) - .font(.custom(Fonts.semiBold, size: 17)) - .foregroundColor(.white) + BodyMSBText(monthYearString) Spacer() Button(action: previousMonth) { - Image(systemName: "chevron.left") + Image("chevron") + .resizable() .foregroundColor(.brandAccent) - .frame(width: 44, height: 44) + .frame(width: 24, height: 24) + .rotationEffect(.degrees(180)) + .frame(width: 44, height: 44) // Increase hit area } .padding(.leading, 8) .accessibilityIdentifier("PrevMonth") Button(action: nextMonth) { - Image(systemName: "chevron.right") + Image("chevron") + .resizable() .foregroundColor(.brandAccent) - .frame(width: 44, height: 44) + .frame(width: 24, height: 24) + .frame(width: 44, height: 44) // Increase hit area } .accessibilityIdentifier("NextMonth") } @@ -98,13 +111,12 @@ struct DateRangeSelectorSheet: View { // Weekday headers HStack(spacing: 0) { - ForEach(calendar.shortWeekdaySymbols, id: \.self) { symbol in - CaptionText(symbol.uppercased()) - .foregroundColor(.white64) + ForEach(0 ..< 7, id: \.self) { index in + let weekdayIndex = (index + calendar.firstWeekday - 1) % 7 + CaptionMText(calendar.shortWeekdaySymbols[weekdayIndex]) .frame(maxWidth: .infinity) } } - .padding(.horizontal, 16) // Calendar grid LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0), count: 7), spacing: 8) { @@ -122,11 +134,10 @@ struct DateRangeSelectorSheet: View { .accessibilityIdentifier(calendar.isDateInToday(date) ? "Today" : "Day-\(calendar.component(.day, from: date))") } else { Color.clear - .frame(height: 40) + .frame(height: 48) } } } - .padding(.horizontal, 16) // Display selected range (fixed height to prevent layout jump) VStack { @@ -172,7 +183,6 @@ struct DateRangeSelectorSheet: View { startDate = nil endDate = nil viewModel.clearDateRange() - isPresented = false } .accessibilityIdentifier("CalendarClearButton") @@ -278,9 +288,23 @@ struct CalendarDayView: View { } else if isStartDate { Circle() .fill(Color.brand16) + UnevenRoundedRectangle( + topLeadingRadius: 44, + bottomLeadingRadius: 44, + bottomTrailingRadius: 0, + topTrailingRadius: 0 + ) + .fill(Color.brand16) } else if isEndDate { Circle() .fill(Color.brand16) + UnevenRoundedRectangle( + topLeadingRadius: 0, + bottomLeadingRadius: 0, + bottomTrailingRadius: 44, + topTrailingRadius: 44 + ) + .fill(Color.brand16) } else { // Middle of range Rectangle() @@ -289,18 +313,16 @@ struct CalendarDayView: View { } // Day number - Text(dayNumber) - .font(.custom(Fonts.regular, size: 16)) - .foregroundColor(isStartDate || isEndDate ? Color.brandAccent : Color.white) + BodyMSBText(dayNumber, textColor: isStartDate || isEndDate ? Color.brandAccent : Color.white) // Today indicator if isToday && !isSelected { Circle() - .stroke(Color.brandAccent, lineWidth: 1) + .fill(Color.white10) } } } - .frame(height: 40) + .frame(height: 48) } } diff --git a/Bitkit/Views/Sheets/Sheet.swift b/Bitkit/Views/Sheets/Sheet.swift index 271c89ac..765fbd2b 100644 --- a/Bitkit/Views/Sheets/Sheet.swift +++ b/Bitkit/Views/Sheets/Sheet.swift @@ -1,7 +1,7 @@ import SwiftUI enum SheetSize { - case small, medium, large + case small, medium, large, calendar var height: CGFloat { let screenHeight = UIScreen.screenHeight @@ -31,6 +31,16 @@ enum SheetSize { return max(minHeight, largePreferredHeight) } return preferredHeight + case .calendar: + let minHeight: CGFloat = 600 + // same as medium + 40px, to be just under search input + let preferredHeight = screenHeight - balanceSpacing + 40 + if preferredHeight < minHeight { + // Use large sheet size when it's too small + let largePreferredHeight = screenHeight - headerSpacing + return max(minHeight, largePreferredHeight) + } + return preferredHeight case .large: let minHeight: CGFloat = 600 // Only Header visible From f12a7a3eb174f54d26c5919b381ccc8e882f82b8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Oct 2025 13:21:29 -0300 Subject: [PATCH 21/21] chore:remove comments --- Bitkit/Components/Activity/DateRangeSelectorSheet.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift index 21a67545..f2946226 100644 --- a/Bitkit/Components/Activity/DateRangeSelectorSheet.swift +++ b/Bitkit/Components/Activity/DateRangeSelectorSheet.swift @@ -44,7 +44,6 @@ struct DateRangeSelectorSheet: View { return [] } - // Get the first day of the month let firstDayOfMonth = monthInterval.start // Find the first day of the week that contains the first day of the month @@ -312,10 +311,8 @@ struct CalendarDayView: View { } } - // Day number BodyMSBText(dayNumber, textColor: isStartDate || isEndDate ? Color.brandAccent : Color.white) - // Today indicator if isToday && !isSelected { Circle() .fill(Color.white10)