| struct AddEntryView: View {
@Environment(\.dismiss) var dismiss
@Binding var entries: [SpotEntry]
var onSave: ((UUID) -> Void)? = nil
var mode: EntryMode = .food
@State private var playName = ""
@State private var dateTime: Date = {
var components = Calendar.current.dateComponents([.year, .month, .day], from: Date())
components.hour = 19
components.minute = 0
return Calendar.current.date(from: components) ?? Date()
}()
@State private var cuisine = ""
@State private var address = ""
@State private var phone = ""
@State private var website = ""
@State private var hours = ""
@State private var neighborhood = ""
@State private var confirmationLink = ""
@State private var isTargeted = false
@State private var selectedPhoto: PhotosPickerItem?
@State private var imageData: Data?
@State private var imageURL = ""
@State private var isLoadingImage = false
@State private var remindersEnabled = false
@State private var notes = ""
@State private var rating = 0
@State private var beenThere = false
@State private var starRating = 0
@State private var showDatePicker = false
@State private var consider = false
var body: some View {
NavigationStack {
Form {
if mode == .show || mode == .event {
Section(header: Text(mode == .show ? "Show Information" : "Event Information").font(.system(size: 12))) {
LabeledContent("Show") {
TextField("Name", text: $playName)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Date & Time") {
DatePicker("", selection: $dateTime, displayedComponents: [.date, .hourAndMinute])
.labelsHidden()
.opacity(consider ? 0 : 1)
.disabled(consider)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Venue") {
TextField("", text: $cuisine)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Address") {
TextField("", text: $address)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Phone") {
TextField("", text: $phone)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
.keyboardType(.phonePad)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Website") {
TextField("", text: $website)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Row / Seat") {
TextField("", text: $neighborhood)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
HStack {
Button {
consider.toggle()
if consider {
var c = Calendar.current.dateComponents([.year, .month, .day], from: Date())
c.hour = 19; c.minute = 0
dateTime = Calendar.current.date(from: c) ?? Date()
}
} label: {
Text("Consider")
.font(.system(size: 14))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(consider ? Color.orange : Color(.systemGray5))
.foregroundColor(consider ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.borderless)
Spacer()
HStack(spacing: 4) {
ForEach(1...5, id: \.self) { star in
Button {
starRating = (starRating == star) ? 0 : star
} label: {
Image(systemName: star <= starRating ? "star.fill" : "star")
.font(.system(size: 18))
.foregroundColor(star <= starRating ? .yellow : .gray.opacity(0.4))
}
.buttonStyle(.borderless)
}
}
.id(starRating)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
} else {
Section(header: Text(modeSectionHeader).font(.system(size: 12))) {
TextField("Name", text: $playName)
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField(modeCuisineLabel, text: $cuisine)
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField("Neighborhood", text: $neighborhood)
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField("Address", text: $address)
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField("Phone", text: $phone)
.font(.system(size: 15))
.keyboardType(.phonePad)
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField("Website", text: $website)
.font(.system(size: 15))
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
TextField("Hours", text: $hours)
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
LabeledContent("Priority") {
HStack(spacing: 4) {
ForEach(1...5, id: \.self) { dot in
Image(systemName: dot <= rating ? "circle.fill" : "circle")
.font(.system(size: 14))
.foregroundColor(dot <= rating ? .primary : .gray.opacity(0.4))
.onTapGesture {
rating = (rating == dot) ? 0 : dot
}
}
}
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
HStack {
Text("Been").font(.system(size: 15))
Spacer()
Toggle("", isOn: $beenThere).labelsHidden()
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
if beenThere {
HStack {
Text("Rating").font(.system(size: 15))
Spacer()
HStack(spacing: 4) {
ForEach(1...5, id: \.self) { star in
Button { starRating = (starRating == star) ? star - 1 : star } label: {
Image(systemName: star <= starRating ? "star.fill" : "star")
.font(.system(size: 14))
.foregroundColor(star <= starRating ? .yellow : .gray.opacity(0.4))
}.buttonStyle(.borderless)
}
}.id(starRating)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
}
}
Section(header: Text("Image").font(.system(size: 12))) {
HStack {
TextField("Image URL", text: $imageURL)
.font(.system(size: 15))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
if isLoadingImage {
ProgressView()
.scaleEffect(0.7)
} else if !imageURL.isEmpty {
Button("Load") {
loadImageFromURL()
}
.font(.system(size: 14))
}
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
PhotosPicker(selection: $selectedPhoto, matching: .images) {
HStack {
if let imageData = imageData,
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 50, height: 67)
.clipped()
.cornerRadius(4)
Text("Change Image")
.font(.system(size: 15))
} else {
Image(systemName: "photo")
.font(.system(size: 15))
Text("Select from Photos")
.font(.system(size: 15))
}
}
}
.onChange(of: selectedPhoto) { _, newValue in
Task {
if let data = try? await newValue?.loadTransferable(type: Data.self) {
imageData = data
}
}
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
if mode != .show && mode != .event {
Section(header: Text("Calendar").font(.system(size: 12))) {
if showDatePicker {
DatePicker("Date & Time", selection: $dateTime, displayedComponents: [.date, .hourAndMinute])
.font(.system(size: 15))
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
} else {
Button(action: { showDatePicker = true }) {
HStack {
Image(systemName: "calendar.badge.plus")
.font(.system(size: 14))
.foregroundColor(.blue)
Text("Add to Calendar")
.font(.system(size: 15))
.foregroundColor(.blue)
Spacer()
}
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
}
}
Section(header: Text("More Info").font(.system(size: 12))) {
TextField("More Info Link", text: $confirmationLink)
.font(.system(size: 15))
.textInputAutocapitalization(.never)
.background(isTargeted ? Color.blue.opacity(0.1) : Color.clear)
.onDrop(of: [.url, .text], isTargeted: $isTargeted) { providers in
handleDrop(providers: providers)
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
Section(header: Text("Reminders").font(.system(size: 12))) {
Toggle(isOn: $remindersEnabled) {
HStack {
Image(systemName: "bell.fill")
.font(.system(size: 14))
.foregroundColor(remindersEnabled ? .orange : .gray)
Text("Enable Reminders")
.font(.system(size: 15))
}
}
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
if remindersEnabled {
Text("Reminders: 1 week, 1 day, 6 hours, 1 hour before.")
.font(.system(size: 13))
.foregroundColor(.secondary)
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
}
Section(header: Text("Notes").font(.system(size: 12))) {
TextField("Add notes...", text: $notes, axis: .vertical)
.font(.system(size: 15))
.lineLimit(3...6)
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
}
}
.environment(\.defaultMinListRowHeight, 0)
.navigationTitle("Add")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
.font(.system(size: 16))
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
let newEntry = SpotEntry(
playName: playName,
dateTime: dateTime,
cuisine: cuisine,
address: address,
phone: phone,
website: website,
neighborhood: neighborhood,
hours: hours,
confirmationLink: confirmationLink,
imageData: imageData.map { EntryStore.resizedImageData($0, maxDimension: 1200) },
remindersEnabled: remindersEnabled,
notes: notes,
rating: rating,
beenThere: beenThere,
starRating: (mode == .show || mode == .event) ? starRating : (beenThere ? starRating : 0),
hasCustomDate: (mode == .show || mode == .event) ? !consider : showDatePicker,
entryMode: mode,
consider: (mode == .show || mode == .event) ? consider : false
)
entries.append(newEntry)
NotificationManager.shared.scheduleReminders(for: newEntry)
onSave?(newEntry.id)
dismiss()
}
.font(.system(size: 16))
.disabled(playName.isEmpty)
}
}
}
}
// MARK: - Mode helpers
private var modeSectionHeader: String {
switch mode {
case .food: return String(localized: "Restaurant Information")
case .show: return String(localized: "Show Information")
case .event: return String(localized: "Event Information")
case .place: return String(localized: "Place Information")
case .shop: return String(localized: "Shop Information")
}
}
private var modeCuisineLabel: String {
switch mode {
case .food: return String(localized: "Cuisine")
case .show: return String(localized: "Venue")
case .event: return String(localized: "Venue")
case .place: return String(localized: "Category")
case .shop: return String(localized: "Category")
}
}
func handleDrop(providers: [NSItemProvider]) -> Bool {
guard let provider = providers.first else { return false }
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, error in
if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) {
DispatchQueue.main.async {
confirmationLink = url.absoluteString
}
}
}
return true
} else if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, error in
if let text = item as? String {
DispatchQueue.main.async {
confirmationLink = text
}
}
}
return true
}
return false
}
func loadImageFromURL() {
guard let url = URL(string: imageURL) else { return }
isLoadingImage = true
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
await MainActor.run {
imageData = data
isLoadingImage = false
}
} catch {
await MainActor.run {
isLoadingImage = false
}
}
}
}
} | `AddEntryView` struct | Defines the `AddEntryView` struct. Conforms to View. |