- Add CustomItineraryItem domain model with sortOrder for ordering - Add CKCustomItineraryItem CloudKit wrapper for persistence - Create CustomItemService for CRUD operations - Create CustomItemSubscriptionService for real-time sync - Add AppDelegate for push notification handling - Add AddItemSheet for creating/editing items - Add CustomItemRow with drag handle - Update TripDetailView with continuous vertical timeline - Enable drag-to-reorder using .draggable/.dropDestination - Add inline "Add" buttons after games and travel segments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
4.4 KiB
Swift
150 lines
4.4 KiB
Swift
//
|
|
// AddItemSheet.swift
|
|
// SportsTime
|
|
//
|
|
// Sheet for adding/editing custom itinerary items
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct AddItemSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
let tripId: UUID
|
|
let anchorDay: Int
|
|
let anchorType: CustomItineraryItem.AnchorType
|
|
let anchorId: String?
|
|
let existingItem: CustomItineraryItem?
|
|
var onSave: (CustomItineraryItem) -> Void
|
|
|
|
@State private var selectedCategory: CustomItineraryItem.ItemCategory = .restaurant
|
|
@State private var title: String = ""
|
|
@State private var isSaving = false
|
|
|
|
private var isEditing: Bool { existingItem != nil }
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: Theme.Spacing.lg) {
|
|
// Category picker
|
|
categoryPicker
|
|
|
|
// Title input
|
|
TextField("What's the plan?", text: $title)
|
|
.textFieldStyle(.roundedBorder)
|
|
.font(.body)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.background(Theme.backgroundGradient(colorScheme))
|
|
.navigationTitle(isEditing ? "Edit Item" : "Add Item")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(isEditing ? "Save" : "Add") {
|
|
saveItem()
|
|
}
|
|
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty || isSaving)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if let existing = existingItem {
|
|
selectedCategory = existing.category
|
|
title = existing.title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var categoryPicker: some View {
|
|
HStack(spacing: Theme.Spacing.md) {
|
|
ForEach(CustomItineraryItem.ItemCategory.allCases, id: \.self) { category in
|
|
CategoryButton(
|
|
category: category,
|
|
isSelected: selectedCategory == category
|
|
) {
|
|
selectedCategory = category
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func saveItem() {
|
|
let trimmedTitle = title.trimmingCharacters(in: .whitespaces)
|
|
guard !trimmedTitle.isEmpty else { return }
|
|
|
|
isSaving = true
|
|
|
|
let item: CustomItineraryItem
|
|
if let existing = existingItem {
|
|
item = CustomItineraryItem(
|
|
id: existing.id,
|
|
tripId: existing.tripId,
|
|
category: selectedCategory,
|
|
title: trimmedTitle,
|
|
anchorType: existing.anchorType,
|
|
anchorId: existing.anchorId,
|
|
anchorDay: existing.anchorDay,
|
|
createdAt: existing.createdAt,
|
|
modifiedAt: Date()
|
|
)
|
|
} else {
|
|
item = CustomItineraryItem(
|
|
tripId: tripId,
|
|
category: selectedCategory,
|
|
title: trimmedTitle,
|
|
anchorType: anchorType,
|
|
anchorId: anchorId,
|
|
anchorDay: anchorDay
|
|
)
|
|
}
|
|
|
|
onSave(item)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
// MARK: - Category Button
|
|
|
|
private struct CategoryButton: View {
|
|
let category: CustomItineraryItem.ItemCategory
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 4) {
|
|
Text(category.icon)
|
|
.font(.title2)
|
|
Text(category.label)
|
|
.font(.caption2)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 12)
|
|
.background(isSelected ? Theme.warmOrange.opacity(0.2) : Color.clear)
|
|
.cornerRadius(8)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(isSelected ? Theme.warmOrange : Color.secondary.opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
AddItemSheet(
|
|
tripId: UUID(),
|
|
anchorDay: 1,
|
|
anchorType: .startOfDay,
|
|
anchorId: nil,
|
|
existingItem: nil
|
|
) { _ in }
|
|
}
|