restore: original UI views from first build, keep fixed models/API
All checks were successful
Security Checks / dependency-audit (push) Successful in 12s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 3s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 01:54:46 -05:00
parent e852e98812
commit fdb8aeba8a
61 changed files with 6652 additions and 1178 deletions

View File

@@ -0,0 +1,108 @@
import Foundation
@MainActor @Observable
final class FoodSearchViewModel {
var searchText = ""
var searchResults: [FoodItem] = []
var recentFoods: [FoodItem] = []
var isSearching = false
var isLoadingRecent = false
var errorMessage: String?
// Add food sheet state
var selectedFood: FoodItem?
var showAddSheet = false
var addQuantity: Double = 1
var addMealType: MealType = .guess()
var isAddingFood = false
private let repo = FitnessRepository.shared
private var searchTask: Task<Void, Never>?
var displayedFoods: [FoodItem] {
if searchText.trimmingCharacters(in: .whitespaces).isEmpty {
return recentFoods
}
return searchResults
}
var isShowingRecent: Bool {
searchText.trimmingCharacters(in: .whitespaces).isEmpty
}
func loadRecent() async {
isLoadingRecent = true
do {
recentFoods = try await repo.recentFoods(forceRefresh: true)
} catch {
// Silent failure for recent foods
}
isLoadingRecent = false
}
func search() {
let query = searchText.trimmingCharacters(in: .whitespaces)
// Cancel previous search
searchTask?.cancel()
guard !query.isEmpty else {
searchResults = []
isSearching = false
return
}
guard query.count >= 2 else {
return
}
isSearching = true
searchTask = Task {
// Debounce
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
do {
let results = try await repo.searchFoods(query: query)
guard !Task.isCancelled else { return }
searchResults = results
} catch {
guard !Task.isCancelled else { return }
errorMessage = error.localizedDescription
}
isSearching = false
}
}
func selectFood(_ food: FoodItem) {
selectedFood = food
addQuantity = 1
addMealType = .guess()
showAddSheet = true
}
func addFood(date: String, onComplete: @escaping () -> Void) async {
guard let food = selectedFood else { return }
isAddingFood = true
let request = CreateEntryRequest(
foodId: food.id,
quantity: addQuantity,
unit: food.baseUnit ?? "serving",
mealType: addMealType.rawValue,
entryDate: date,
entryMethod: "manual",
source: "ios_app"
)
do {
_ = try await repo.createEntry(request)
showAddSheet = false
selectedFood = nil
onComplete()
} catch {
errorMessage = "Failed to add food: \(error.localizedDescription)"
}
isAddingFood = false
}
}

View File

@@ -0,0 +1,23 @@
import Foundation
@MainActor @Observable
final class GoalsViewModel {
var goal: DailyGoal = .defaultGoal
var isLoading = true
var errorMessage: String?
private let repo = FitnessRepository.shared
func load() async {
isLoading = true
errorMessage = nil
do {
goal = try await repo.goals(for: Date().apiDateString, forceRefresh: true)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}

View File

@@ -0,0 +1,76 @@
import Foundation
@MainActor @Observable
final class HistoryViewModel {
var days: [HistoryDay] = []
var isLoading = true
var errorMessage: String?
private let repo = FitnessRepository.shared
private let numberOfDays = 14
struct HistoryDay: Identifiable {
let date: Date
let dateString: String
let entries: [FoodEntry]
let goal: DailyGoal
var id: String { dateString }
var totalCalories: Double {
entries.reduce(0) { $0 + $1.calories }
}
var totalProtein: Double {
entries.reduce(0) { $0 + $1.protein }
}
var totalCarbs: Double {
entries.reduce(0) { $0 + $1.carbs }
}
var totalFat: Double {
entries.reduce(0) { $0 + $1.fat }
}
var entryCount: Int {
entries.count
}
var calorieProgress: Double {
guard goal.calories > 0 else { return 0 }
return min(totalCalories / goal.calories, 1.0)
}
}
func load() async {
isLoading = true
errorMessage = nil
var results: [HistoryDay] = []
do {
// Load past N days
for i in 0..<numberOfDays {
let date = Date().adding(days: -i)
let dateString = date.apiDateString
let entries = try await repo.entries(for: dateString, forceRefresh: i == 0)
let goal = try await repo.goals(for: dateString)
results.append(HistoryDay(
date: date,
dateString: dateString,
entries: entries,
goal: goal
))
}
days = results
} catch {
errorMessage = error.localizedDescription
days = results // Show what we have
}
isLoading = false
}
}

View File

@@ -0,0 +1,45 @@
import Foundation
@MainActor @Observable
final class TemplatesViewModel {
var templates: [MealTemplate] = []
var isLoading = true
var errorMessage: String?
var isLogging = false
var loggedTemplateId: String?
private let repo = FitnessRepository.shared
func load() async {
isLoading = true
errorMessage = nil
do {
templates = try await repo.templates(forceRefresh: true)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func logTemplate(_ template: MealTemplate, date: String, onComplete: @escaping () -> Void) async {
isLogging = true
loggedTemplateId = template.id
do {
try await repo.logTemplate(id: template.id, date: date)
loggedTemplateId = nil
onComplete()
} catch {
errorMessage = "Failed to log template: \(error.localizedDescription)"
loggedTemplateId = nil
}
isLogging = false
}
var groupedTemplates: [String: [MealTemplate]] {
Dictionary(grouping: templates, by: \.mealType)
}
}

View File

@@ -0,0 +1,111 @@
import Foundation
@MainActor @Observable
final class TodayViewModel {
var entries: [FoodEntry] = []
var goal: DailyGoal = .defaultGoal
var selectedDate: Date = Date()
var isLoading = true
var errorMessage: String?
var expandedMeals: Set<String> = Set(MealType.allCases.map(\.rawValue))
private let repo = FitnessRepository.shared
// MARK: - Computed Properties
var dateString: String {
selectedDate.apiDateString
}
var mealGroups: [MealGroup] {
MealType.allCases.map { meal in
MealGroup(
meal: meal,
entries: entries.filter { $0.mealType == meal.rawValue }
)
}
}
var totalCalories: Double {
entries.reduce(0) { $0 + $1.calories }
}
var totalProtein: Double {
entries.reduce(0) { $0 + $1.protein }
}
var totalCarbs: Double {
entries.reduce(0) { $0 + $1.carbs }
}
var totalFat: Double {
entries.reduce(0) { $0 + $1.fat }
}
var caloriesRemaining: Double {
max(goal.calories - totalCalories, 0)
}
// MARK: - Actions
func load() async {
isLoading = true
errorMessage = nil
do {
async let entriesTask = repo.entries(for: dateString, forceRefresh: true)
async let goalsTask = repo.goals(for: dateString)
entries = try await entriesTask
goal = try await goalsTask
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func goToNextDay() {
selectedDate = selectedDate.adding(days: 1)
Task { await load() }
}
func goToPreviousDay() {
selectedDate = selectedDate.adding(days: -1)
Task { await load() }
}
func goToToday() {
selectedDate = Date()
Task { await load() }
}
func toggleMeal(_ meal: String) {
if expandedMeals.contains(meal) {
expandedMeals.remove(meal)
} else {
expandedMeals.insert(meal)
}
}
func deleteEntry(_ entry: FoodEntry) async {
// Optimistic removal
entries.removeAll { $0.id == entry.id }
do {
try await repo.deleteEntry(id: entry.id, date: dateString)
} catch {
// Reload on failure
await load()
}
}
func updateEntryQuantity(id: String, quantity: Double) async {
let request = UpdateEntryRequest(quantity: quantity)
do {
_ = try await repo.updateEntry(id: id, request: request, date: dateString)
await load()
} catch {
errorMessage = "Failed to update entry"
}
}
}