Add shared utilities and refactor iOS codebase for DRY compliance
Create centralized shared utilities in iosApp/Shared/: - Extensions: ViewExtensions, DateExtensions, StringExtensions, DoubleExtensions - Components: FormComponents, SharedEmptyStateView, ButtonStyles - Modifiers: CardModifiers - Utilities: ValidationHelpers, ErrorMessages Migrate existing views to use shared utilities: - LoginView: Use IconTextField, FieldLabel, FieldError, OrganicPrimaryButton - TaskFormView: Use .loadingOverlay() modifier - TaskCard/DynamicTaskCard: Use .toFormattedDate() extension - CompletionCardView: Use .toCurrency() (with KotlinDouble support) - ResidenceDetailView: Use OrganicEmptyState, StandardLoadingView - Profile views: Use .standardFormStyle(), .sectionBackground() - Form views: Use consistent form styling modifiers Benefits: - Eliminates ~180 lines of duplicate code - Consistent styling across all forms and components - KotlinDouble extensions for seamless KMM interop - Single source of truth for UI patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,9 +67,7 @@ struct TaskFormView: View {
|
||||
_inProgress = State(initialValue: task.inProgress)
|
||||
|
||||
// Parse date from string - use effective due date (nextDueDate if set, otherwise dueDate)
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
_dueDate = State(initialValue: formatter.date(from: task.effectiveDueDate ?? "") ?? Date())
|
||||
_dueDate = State(initialValue: (task.effectiveDueDate ?? "").toDate() ?? Date())
|
||||
|
||||
_intervalDays = State(initialValue: task.customIntervalDays != nil ? String(task.customIntervalDays!.int32Value) : "")
|
||||
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
|
||||
@@ -97,11 +95,7 @@ struct TaskFormView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
WarmGradientBackground()
|
||||
.ignoresSafeArea()
|
||||
|
||||
Form {
|
||||
Form {
|
||||
// Residence Picker (only if needed)
|
||||
if needsResidenceSelection, let residences = residences {
|
||||
Section {
|
||||
@@ -113,9 +107,7 @@ struct TaskFormView: View {
|
||||
}
|
||||
|
||||
if !residenceError.isEmpty {
|
||||
Text(residenceError)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
FieldError(message: residenceError)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.Tasks.property)
|
||||
@@ -124,7 +116,7 @@ struct TaskFormView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
}
|
||||
|
||||
// Browse Templates Button (only for new tasks)
|
||||
@@ -169,7 +161,7 @@ struct TaskFormView: View {
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -192,9 +184,7 @@ struct TaskFormView: View {
|
||||
}
|
||||
|
||||
if !titleError.isEmpty {
|
||||
Text(titleError)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
FieldError(message: titleError)
|
||||
}
|
||||
|
||||
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
|
||||
@@ -208,7 +198,7 @@ struct TaskFormView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
|
||||
Section {
|
||||
Picker(L10n.Tasks.category, selection: $selectedCategory) {
|
||||
@@ -224,7 +214,7 @@ struct TaskFormView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
|
||||
Section {
|
||||
Picker(L10n.Tasks.frequency, selection: $selectedFrequency) {
|
||||
@@ -262,7 +252,7 @@ struct TaskFormView: View {
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
|
||||
Section {
|
||||
Picker(L10n.Tasks.priority, selection: $selectedPriority) {
|
||||
@@ -280,14 +270,14 @@ struct TaskFormView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
|
||||
Section(header: Text(L10n.Tasks.cost)) {
|
||||
TextField(L10n.Tasks.estimatedCost, text: $estimatedCost)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
.keyboardDismissToolbar()
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
@@ -296,43 +286,11 @@ struct TaskFormView: View {
|
||||
.foregroundColor(Color.appError)
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.sectionBackground()
|
||||
}
|
||||
}
|
||||
.disabled(isLoadingLookups)
|
||||
.blur(radius: isLoadingLookups ? 3 : 0)
|
||||
|
||||
if isLoadingLookups {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 64, height: 64)
|
||||
ProgressView()
|
||||
.scaleEffect(1.2)
|
||||
.tint(Color.appPrimary)
|
||||
}
|
||||
Text(L10n.Tasks.loading)
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.padding(OrganicSpacing.spacious)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
||||
.fill(Color.appBackgroundSecondary)
|
||||
.overlay(
|
||||
GrainTexture(opacity: 0.015)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||
)
|
||||
)
|
||||
.naturalShadow(.medium)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.appBackgroundPrimary.opacity(0.9))
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.clear)
|
||||
.loadingOverlay(isLoading: isLoadingLookups, message: L10n.Tasks.loading)
|
||||
.standardFormStyle()
|
||||
.navigationTitle(isEditMode ? L10n.Tasks.editTitle : L10n.Tasks.addTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -498,10 +456,8 @@ struct TaskFormView: View {
|
||||
return
|
||||
}
|
||||
|
||||
// Format date as yyyy-MM-dd
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
let dueDateString = dateFormatter.string(from: dueDate)
|
||||
// Format date as yyyy-MM-dd using extension
|
||||
let dueDateString = dueDate.formattedAPI()
|
||||
|
||||
if isEditMode, let task = existingTask {
|
||||
// UPDATE existing task
|
||||
|
||||
Reference in New Issue
Block a user