import SwiftUI import SwiftData struct DishFormView: View { @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss @Query private var tags: [Tag] @Query private var dishes: [Dish] @Query private var allSettings: [AppSettings] @StateObject private var viewModel = DishViewModel() @State private var showPremium = false @State private var showDishLimitUpsell = false var dish: Dish? private var language: AppLanguage { (allSettings.first?.languageEnum ?? .system).resolved() } private var reachedFreeDishLimit: Bool { guard let settings = allSettings.first else { return false } if viewModel.isEditing { return false } return PremiumAccess.hasReachedFreeDishLimit(dishCount: dishes.count, isPremium: settings.isPremium) } var body: some View { NavigationStack { ZStack { Color.mealMoodBackground.ignoresSafeArea() ScrollView { VStack(spacing: 20) { // Name field VStack(alignment: .leading, spacing: 8) { Text("dish_name_label") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) TextField( text: $viewModel.name, prompt: Text("dish_name_placeholder").foregroundColor(.mealMoodTextSecondary.opacity(0.6)) ) { EmptyView() } .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) .tint(.mealMoodCoral) .padding(14) .background(Color.mealMoodSurface) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) ) } // Description field VStack(alignment: .leading, spacing: 8) { Text("dish_description_label") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) TextField( text: $viewModel.descriptionText, prompt: Text("dish_description_placeholder").foregroundColor(.mealMoodTextSecondary.opacity(0.6)), axis: .vertical ) { EmptyView() } .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) .tint(.mealMoodCoral) .lineLimit(2...4) .padding(14) .background(Color.mealMoodSurface) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) ) } // Tags section VStack(alignment: .leading, spacing: 8) { Text("dish_tags_label") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) if !viewModel.selectedTagIds.isEmpty { FlowLayout(spacing: 6) { ForEach(tags.filter { viewModel.selectedTagIds.contains($0.id) }) { tag in HStack(spacing: 4) { TagPill(name: tag.localizedName(language: language), color: tag.color) Button { viewModel.selectedTagIds.remove(tag.id) } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 14)) .foregroundColor(.mealMoodTextSecondary) } } } } } Button { viewModel.showTagSelector = true } label: { HStack { Image(systemName: "plus.circle") Text("dish_add_tag") } .font(.mealMoodSmall) .foregroundColor(.mealMoodCoral) } } if showDishLimitUpsell && reachedFreeDishLimit { PremiumUpsellBanner( messageKey: "premium_limit_dishes", actionTitleKey: "premium_subscribe" ) { showPremium = true } } // Delete button (edit mode only) if viewModel.isEditing { Button { viewModel.showDeleteAlert = true } label: { HStack { Image(systemName: "trash") Text("dish_delete") } .font(.mealMoodBodyBold) .foregroundColor(.mealMoodError) .padding(.vertical, 14) .frame(maxWidth: .infinity) .background(Color.mealMoodError.opacity(0.1)) .cornerRadius(14) } .padding(.top, 20) } } .padding(24) } } .navigationTitle(viewModel.isEditing ? String(localized: "dish_edit_title") : String(localized: "dish_new_title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("dish_cancel") { dismiss() } .foregroundColor(.mealMoodTextSecondary) } ToolbarItem(placement: .navigationBarTrailing) { Button { guard !reachedFreeDishLimit else { withAnimation(.easeInOut(duration: 0.2)) { showDishLimitUpsell = true } showPremium = true HapticManager.shared.notification(type: .warning) return } viewModel.save(context: context) dismiss() } label: { Image(systemName: "checkmark") .fontWeight(.semibold) .foregroundColor(viewModel.isValid ? .mealMoodCoral : .gray) } .disabled(!viewModel.isValid) } } .sheet(isPresented: $viewModel.showTagSelector) { TagSelectorSheet(tags: tags, selectedTagIds: $viewModel.selectedTagIds) } .sheet(isPresented: $showPremium) { if let settings = allSettings.first { NavigationStack { PremiumView(settings: settings) } } } .alert("dish_delete_title", isPresented: $viewModel.showDeleteAlert) { Button("dish_cancel", role: .cancel) {} Button("dish_delete", role: .destructive) { let deleted = viewModel.deleteDish(context: context) if deleted { dismiss() } } } message: { Text("dish_delete_message") } .alert("dish_delete_blocked_title", isPresented: $viewModel.showAssignedDeleteAlert) { Button("dish_delete_blocked_ok", role: .cancel) {} } message: { Text("dish_delete_blocked_message") } .onAppear { if let dish = dish { viewModel.loadDish(dish) } } } } } // Simple flow layout for tags struct FlowLayout: Layout { var spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let result = arrange(proposal: proposal, subviews: subviews) return result.size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let result = arrange(proposal: proposal, subviews: subviews) for (index, position) in result.positions.enumerated() { subviews[index].place(at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), proposal: .unspecified) } } private func arrange(proposal: ProposedViewSize, subviews: Subviews) -> (size: CGSize, positions: [CGPoint]) { let maxWidth = proposal.width ?? .infinity var positions: [CGPoint] = [] var x: CGFloat = 0 var y: CGFloat = 0 var maxHeight: CGFloat = 0 var rowHeight: CGFloat = 0 for subview in subviews { let size = subview.sizeThatFits(.unspecified) if x + size.width > maxWidth && x > 0 { x = 0 y += rowHeight + spacing rowHeight = 0 } positions.append(CGPoint(x: x, y: y)) rowHeight = max(rowHeight, size.height) x += size.width + spacing maxHeight = max(maxHeight, y + rowHeight) } return (CGSize(width: maxWidth, height: maxHeight), positions) } }