Version casi lista
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import SwiftUI
|
||||
import EventKit
|
||||
|
||||
struct CalendarStepView: View {
|
||||
@Binding var syncEnabled: Bool
|
||||
@Binding var selectedCalendarId: String?
|
||||
@Binding var lunchTime: Date
|
||||
@Binding var dinnerTime: Date
|
||||
var onNext: () -> Void
|
||||
var onSkip: () -> Void
|
||||
|
||||
@State private var availableCalendars: [EKCalendar] = []
|
||||
@State private var showPermissionAlert = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
Text("calendar_title")
|
||||
.font(.mealMoodH2)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 32)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
// Sync toggle
|
||||
HStack {
|
||||
Text("calendar_sync")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
Spacer()
|
||||
Toggle("", isOn: $syncEnabled)
|
||||
.tint(.mealMoodCoral)
|
||||
.labelsHidden()
|
||||
.onChange(of: syncEnabled) { _, newValue in
|
||||
if newValue {
|
||||
Task {
|
||||
let granted = await CalendarService.shared.requestAccess()
|
||||
if granted {
|
||||
availableCalendars = CalendarService.shared.availableCalendars()
|
||||
} else {
|
||||
syncEnabled = false
|
||||
showPermissionAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
||||
)
|
||||
|
||||
if syncEnabled {
|
||||
// Calendar picker
|
||||
if !availableCalendars.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("calendar_select")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
|
||||
Picker("calendar_select", selection: $selectedCalendarId) {
|
||||
Text("calendar_select").tag(nil as String?)
|
||||
ForEach(availableCalendars, id: \.calendarIdentifier) { cal in
|
||||
Text(cal.title).tag(cal.calendarIdentifier as String?)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.padding(12)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Time pickers
|
||||
VStack(spacing: 12) {
|
||||
DatePicker("calendar_lunch_time", selection: $lunchTime, displayedComponents: .hourAndMinute)
|
||||
.font(.mealMoodBody)
|
||||
|
||||
DatePicker("calendar_dinner_time", selection: $dinnerTime, displayedComponents: .hourAndMinute)
|
||||
.font(.mealMoodBody)
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Text("calendar_optional")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
|
||||
Spacer(minLength: 40)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
SecondaryButton(title: String(localized: "onboarding_skip"), action: {
|
||||
syncEnabled = false
|
||||
onSkip()
|
||||
})
|
||||
|
||||
PrimaryButton(title: String(localized: "onboarding_continue"), action: onNext)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
.alert("calendar_permission_title", isPresented: $showPermissionAlert) {
|
||||
Button("calendar_permission_settings") {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
Button("reset_cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("calendar_permission_message")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MealWindowsStepView: View {
|
||||
@Binding var selection: MealWindows
|
||||
var onNext: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
Text("meal_windows_title")
|
||||
.font(.mealMoodH2)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ForEach(MealWindows.allCases, id: \.self) { option in
|
||||
SelectionCard(
|
||||
icon: option.icon,
|
||||
title: optionTitle(option),
|
||||
isSelected: selection == option
|
||||
) {
|
||||
withAnimation(.spring(response: 0.3)) {
|
||||
selection = option
|
||||
}
|
||||
HapticManager.shared.selection()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
PrimaryButton(title: String(localized: "onboarding_continue"), action: onNext)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
|
||||
private func optionTitle(_ option: MealWindows) -> String {
|
||||
switch option {
|
||||
case .dinnerOnly: return "🌙 \(String(localized: "meal_windows_dinner_only"))"
|
||||
case .lunchOnly: return "☀️ \(String(localized: "meal_windows_lunch_only"))"
|
||||
case .both: return "🌞🌙 \(String(localized: "meal_windows_both"))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectionCard: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
Spacer()
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundColor(isSelected ? .mealMoodCoral : Color(hex: "#C4C4C4"))
|
||||
.font(.system(size: 22))
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(isSelected ? Color.mealMoodCoral : Color(hex: "#F0F0F0"), lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct OnboardingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
@StateObject private var viewModel = OnboardingViewModel()
|
||||
@Query(sort: \Tag.sortOrder) private var tags: [Tag]
|
||||
var onComplete: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.mealMoodBackground.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Progress indicator
|
||||
if viewModel.currentStep > 0 {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(1..<viewModel.totalSteps, id: \.self) { step in
|
||||
Capsule()
|
||||
.fill(step <= viewModel.currentStep ? Color.mealMoodCoral : Color(hex: "#E0E0E0"))
|
||||
.frame(height: 4)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
|
||||
TabView(selection: $viewModel.currentStep) {
|
||||
WelcomeStepView(onNext: { viewModel.nextStep() })
|
||||
.tag(0)
|
||||
|
||||
MealWindowsStepView(
|
||||
selection: $viewModel.selectedMealWindows,
|
||||
onNext: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(1)
|
||||
|
||||
WeekendsStepView(
|
||||
includeWeekends: $viewModel.includeWeekends,
|
||||
onNext: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(2)
|
||||
|
||||
CalendarStepView(
|
||||
syncEnabled: $viewModel.syncCalendar,
|
||||
selectedCalendarId: $viewModel.selectedCalendarId,
|
||||
lunchTime: $viewModel.lunchTime,
|
||||
dinnerTime: $viewModel.dinnerTime,
|
||||
onNext: { viewModel.nextStep() },
|
||||
onSkip: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(3)
|
||||
|
||||
FirstDishesStepView(
|
||||
viewModel: viewModel,
|
||||
tags: tags,
|
||||
onFinish: {
|
||||
if viewModel.completeOnboarding(context: context) {
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
)
|
||||
.tag(4)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
ensureDefaultTagsIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureDefaultTagsIfNeeded() {
|
||||
let descriptor = FetchDescriptor<Tag>()
|
||||
if (try? context.fetch(descriptor))?.isEmpty ?? true {
|
||||
DefaultDataService.createDefaultTags(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WeekendsStepView: View {
|
||||
@Binding var includeWeekends: Bool
|
||||
var onNext: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
Text("weekends_title")
|
||||
.font(.mealMoodH2)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("weekends_toggle")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $includeWeekends)
|
||||
.tint(.mealMoodCoral)
|
||||
.labelsHidden()
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Text("weekends_note")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
PrimaryButton(title: String(localized: "onboarding_continue"), action: onNext)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WelcomeStepView: View {
|
||||
var onNext: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 32) {
|
||||
Spacer()
|
||||
|
||||
AppIconPlaceholder(size: 120)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text("onboarding_welcome_title")
|
||||
.font(.mealMoodH1)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
|
||||
Text("onboarding_welcome_subtitle")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Image(systemName: "figure.2.and.child.holdinghands")
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.mealMoodCoral, .mealMoodMint],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.padding(.vertical, 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
BenefitRow(icon: "face.smiling", text: String(localized: "onboarding_welcome_benefit1"))
|
||||
BenefitRow(icon: "scalemass", text: String(localized: "onboarding_welcome_benefit2"))
|
||||
BenefitRow(icon: "calendar", text: String(localized: "onboarding_welcome_benefit3"))
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
Spacer()
|
||||
|
||||
PrimaryButton(title: String(localized: "onboarding_start"), icon: "✨", action: onNext)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BenefitRow: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.mealMoodCoral)
|
||||
.frame(width: 28)
|
||||
|
||||
Text(text)
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user