Version casi lista

This commit is contained in:
alexandrev-tibco
2026-02-17 13:34:02 +01:00
commit e593453abd
99 changed files with 10867 additions and 0 deletions
@@ -0,0 +1,256 @@
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
}
var body: some View {
ScrollView {
VStack(spacing: 24) {
VStack(alignment: .leading, spacing: 8) {
Text("first_dishes_title")
.font(.mealMoodH2)
.foregroundColor(.mealMoodTextPrimary)
Text("first_dishes_subtitle")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.top, 32)
// 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)
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)
.padding(.horizontal, 24)
}
}
}
VStack(alignment: .leading, spacing: 10) {
Text("first_dishes_suggestions")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
.padding(.horizontal, 24)
ForEach(suggestedDishes(), id: \.name) { suggestion in
let isAdded = viewModel.addedDishes.contains(where: {
$0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == suggestion.name.lowercased()
})
Button {
guard !isAdded else { return }
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(isAdded ? "first_dishes_suggestion_added" : "first_dishes_suggestion_add")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
}
Spacer()
Image(systemName: isAdded ? "checkmark.circle.fill" : "plus.circle.fill")
.foregroundColor(isAdded ? .mealMoodSuccess : .mealMoodCoral)
}
.padding(12)
.background(Color.mealMoodSurface)
.cornerRadius(12)
}
.disabled(isAdded)
.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()
}
}
}
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"])
]
}
return [
(name: "Pollo asado", tagHints: ["Carne"]),
(name: "Lentejas estofadas", tagHints: ["Legumbres"]),
(name: "Pasta con verduras", tagHints: ["Pasta/Arroz", "Verduras"])
]
}
private func ensureDefaultTagsIfNeeded() {
let descriptor = FetchDescriptor<Tag>()
if (try? context.fetch(descriptor))?.isEmpty ?? true {
DefaultDataService.createDefaultTags(context: context)
}
}
}