297 lines
13 KiB
Swift
297 lines
13 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct FirstDishesStepView: View {
|
|
@Environment(\.modelContext) private var context
|
|
@ObservedObject var viewModel: OnboardingViewModel
|
|
let tags: [Tag]
|
|
var onFinish: () -> Void
|
|
|
|
@State private var showTagSelector = false
|
|
@Query(sort: \Tag.sortOrder) private var storedTags: [Tag]
|
|
@Query private var allSettings: [AppSettings]
|
|
|
|
private var language: AppLanguage {
|
|
(allSettings.first?.languageEnum ?? .system).resolved()
|
|
}
|
|
|
|
private var availableTags: [Tag] {
|
|
var seen = Set<UUID>()
|
|
let merged = (tags + storedTags).filter { tag in
|
|
seen.insert(tag.id).inserted
|
|
}
|
|
return merged.sorted(by: { $0.sortOrder < $1.sortOrder })
|
|
}
|
|
|
|
private var defaultTagId: UUID? {
|
|
availableTags.first(where: { $0.name.caseInsensitiveCompare("Verduras") == .orderedSame })?.id
|
|
?? availableTags.first(where: { $0.nameEN.caseInsensitiveCompare("Vegetables") == .orderedSame })?.id
|
|
?? availableTags.sorted(by: { $0.sortOrder < $1.sortOrder }).first?.id
|
|
}
|
|
|
|
private let maxVisibleSuggestions = 3
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
Text("first_dishes_title")
|
|
.font(.mealMoodH1)
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, 12)
|
|
|
|
Text("first_dishes_subtitle")
|
|
.font(.mealMoodSmall)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 24)
|
|
|
|
// New dish form
|
|
VStack(spacing: 12) {
|
|
TextField("first_dishes_name_placeholder", text: $viewModel.newDishName)
|
|
.font(.mealMoodBody)
|
|
.padding(14)
|
|
.background(Color.mealMoodSurface)
|
|
.cornerRadius(12)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
|
)
|
|
|
|
// Selected tags
|
|
if !viewModel.newDishTags.isEmpty {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 6) {
|
|
ForEach(availableTags.filter { viewModel.newDishTags.contains($0.id) }) { tag in
|
|
HStack(spacing: 4) {
|
|
TagPill(name: tag.localizedName(language: language), color: tag.color)
|
|
Button {
|
|
viewModel.newDishTags.remove(tag.id)
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
ensureDefaultTagsIfNeeded()
|
|
showTagSelector = true
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "plus.circle")
|
|
Text("first_dishes_add_tag")
|
|
}
|
|
.font(.mealMoodSmall)
|
|
.foregroundColor(.mealMoodCoral)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
if availableTags.isEmpty {
|
|
Text("first_dishes_no_tags")
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodError)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
Button {
|
|
ensureDefaultTagsIfNeeded()
|
|
viewModel.addDish(defaultTagId: defaultTagId)
|
|
HapticManager.shared.notification(type: .success)
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "plus")
|
|
Text("first_dishes_add_dish")
|
|
}
|
|
.font(.mealMoodBodyBold)
|
|
.foregroundColor(.white)
|
|
.padding(.vertical, 12)
|
|
.frame(maxWidth: .infinity)
|
|
.background(viewModel.canAddDish ? Color.mealMoodCoral : Color.gray.opacity(0.3))
|
|
.cornerRadius(12)
|
|
}
|
|
.disabled(!viewModel.canAddDish)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
// Added dishes list
|
|
if !viewModel.addedDishes.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("first_dishes_added")
|
|
.font(.mealMoodSmall)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView {
|
|
LazyVStack(spacing: 8) {
|
|
ForEach(Array(viewModel.addedDishes.enumerated()), id: \.offset) { index, dish in
|
|
HStack {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundColor(.mealMoodSuccess)
|
|
|
|
Text(dish.name)
|
|
.font(.mealMoodBody)
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
|
|
// Show tag pills
|
|
HStack(spacing: 4) {
|
|
ForEach(availableTags.filter { dish.tagIds.contains($0.id) }.prefix(2)) { tag in
|
|
TagPill(name: tag.localizedName(language: language), color: tag.color)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
viewModel.removeDish(at: index)
|
|
} label: {
|
|
Image(systemName: "trash")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.mealMoodError)
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(Color.mealMoodSurface)
|
|
.cornerRadius(12)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxHeight: 190)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text("first_dishes_suggestions")
|
|
.font(.mealMoodSmall)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.padding(.horizontal, 24)
|
|
|
|
Text("Added \(viewModel.addedDishes.count)/\(viewModel.maxOnboardingDishes)")
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.padding(.horizontal, 24)
|
|
|
|
ForEach(visibleSuggestions(), id: \.name) { suggestion in
|
|
Button {
|
|
viewModel.addSuggestedDish(
|
|
name: suggestion.name,
|
|
preferredTagNames: suggestion.tagHints,
|
|
availableTags: availableTags
|
|
)
|
|
} label: {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(suggestion.name)
|
|
.font(.mealMoodBodyBold)
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
Text("first_dishes_suggestion_add")
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
}
|
|
Spacer()
|
|
Image(systemName: "plus.circle.fill")
|
|
.foregroundColor(.mealMoodCoral)
|
|
}
|
|
.padding(12)
|
|
.background(Color.mealMoodSurface)
|
|
.cornerRadius(12)
|
|
}
|
|
.disabled(!viewModel.canAddMoreDishes)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
|
|
if !viewModel.canAddMoreDishes {
|
|
Text("first_dishes_limit_reached")
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 40)
|
|
|
|
PrimaryButton(
|
|
title: String(localized: "onboarding_finish"),
|
|
icon: "🎉",
|
|
action: {
|
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
HapticManager.shared.notification(type: .success)
|
|
onFinish()
|
|
},
|
|
isEnabled: viewModel.addedDishes.count >= 2
|
|
)
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 40)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showTagSelector) {
|
|
TagSelectorSheet(
|
|
tags: availableTags,
|
|
selectedTagIds: $viewModel.newDishTags
|
|
)
|
|
}
|
|
.onAppear {
|
|
ensureDefaultTagsIfNeeded()
|
|
}
|
|
.onChange(of: storedTags.count) { _, newCount in
|
|
if newCount == 0 {
|
|
ensureDefaultTagsIfNeeded()
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
}
|
|
|
|
private func suggestedDishes() -> [(name: String, tagHints: [String])] {
|
|
if language == .english {
|
|
return [
|
|
(name: "Roast chicken", tagHints: ["Meat"]),
|
|
(name: "Lentil stew", tagHints: ["Legumes"]),
|
|
(name: "Veggie pasta", tagHints: ["Pasta/Rice", "Vegetables"]),
|
|
(name: "Chicken curry", tagHints: ["Meat"]),
|
|
(name: "Salmon with rice", tagHints: ["Fish", "Pasta/Rice"]),
|
|
(name: "Turkey meatballs", tagHints: ["Meat"]),
|
|
(name: "Chickpea salad", tagHints: ["Legumes", "Vegetables"]),
|
|
(name: "Beef tacos", tagHints: ["Meat"]),
|
|
(name: "Vegetable soup", tagHints: ["Vegetables"]),
|
|
(name: "Baked cod", tagHints: ["Fish"])
|
|
]
|
|
}
|
|
|
|
return [
|
|
(name: "Pollo asado", tagHints: ["Carne"]),
|
|
(name: "Lentejas estofadas", tagHints: ["Legumbres"]),
|
|
(name: "Pasta con verduras", tagHints: ["Pasta/Arroz", "Verduras"]),
|
|
(name: "Pollo al curry", tagHints: ["Carne"]),
|
|
(name: "Salmón con arroz", tagHints: ["Pescado", "Pasta/Arroz"]),
|
|
(name: "Albóndigas de pavo", tagHints: ["Carne"]),
|
|
(name: "Ensalada de garbanzos", tagHints: ["Legumbres", "Verduras"]),
|
|
(name: "Tacos de ternera", tagHints: ["Carne"]),
|
|
(name: "Crema de verduras", tagHints: ["Verduras"]),
|
|
(name: "Merluza al horno", tagHints: ["Pescado"])
|
|
]
|
|
}
|
|
|
|
private func visibleSuggestions() -> [(name: String, tagHints: [String])] {
|
|
let addedNames = Set(
|
|
viewModel.addedDishes.map {
|
|
$0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
}
|
|
)
|
|
let remaining = suggestedDishes().filter { !addedNames.contains($0.name.lowercased()) }
|
|
return Array(remaining.prefix(maxVisibleSuggestions))
|
|
}
|
|
|
|
private func ensureDefaultTagsIfNeeded() {
|
|
let descriptor = FetchDescriptor<Tag>()
|
|
if (try? context.fetch(descriptor))?.isEmpty ?? true {
|
|
DefaultDataService.createDefaultTags(context: context)
|
|
}
|
|
}
|
|
}
|