restore: original UI views from first build, keep fixed models/API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
188
ios/Platform 1.44.16 AM/Platform/Features/Home/HomeView.swift
Normal file
188
ios/Platform 1.44.16 AM/Platform/Features/Home/HomeView.swift
Normal file
@@ -0,0 +1,188 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@Environment(AuthManager.self) private var authManager
|
||||
@State private var viewModel = HomeViewModel()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
if viewModel.isLoading {
|
||||
LoadingView(message: "Loading dashboard...")
|
||||
.frame(height: 300)
|
||||
} else {
|
||||
// Quick Stats Card
|
||||
caloriesSummaryCard
|
||||
|
||||
// Macros Card
|
||||
macrosCard
|
||||
|
||||
// Quick Actions
|
||||
quickActionsCard
|
||||
}
|
||||
|
||||
if let error = viewModel.errorMessage {
|
||||
ErrorBanner(message: error) {
|
||||
Task { await viewModel.load() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.background(Color.canvas)
|
||||
.navigationTitle("Dashboard")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
Button(role: .destructive) {
|
||||
authManager.logout()
|
||||
} label: {
|
||||
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(Color.accentWarm)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.load()
|
||||
}
|
||||
.task {
|
||||
await viewModel.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var caloriesSummaryCard: some View {
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Today")
|
||||
.font(.headline)
|
||||
.foregroundStyle(Color.text1)
|
||||
Text(Date().displayString)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.text3)
|
||||
}
|
||||
Spacer()
|
||||
Text("\(viewModel.entryCount) entries")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.text4)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.surfaceSecondary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
MacroRingLarge(
|
||||
current: viewModel.totalCalories,
|
||||
goal: viewModel.goal.calories,
|
||||
color: .caloriesColor,
|
||||
size: 140,
|
||||
lineWidth: 12
|
||||
)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
macroStat("Eaten", value: Int(viewModel.totalCalories), unit: "kcal")
|
||||
Spacer()
|
||||
macroStat("Remaining", value: Int(max(viewModel.goal.calories - viewModel.totalCalories, 0)), unit: "kcal")
|
||||
Spacer()
|
||||
macroStat("Goal", value: Int(viewModel.goal.calories), unit: "kcal")
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.04), radius: 8, y: 4)
|
||||
}
|
||||
|
||||
private var macrosCard: some View {
|
||||
VStack(spacing: 14) {
|
||||
Text("Macros")
|
||||
.font(.headline)
|
||||
.foregroundStyle(Color.text1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
HStack(spacing: 20) {
|
||||
MacroRing(
|
||||
current: viewModel.totalProtein,
|
||||
goal: viewModel.goal.protein,
|
||||
color: .proteinColor,
|
||||
label: "Protein",
|
||||
unit: "g",
|
||||
size: 68
|
||||
)
|
||||
MacroRing(
|
||||
current: viewModel.totalCarbs,
|
||||
goal: viewModel.goal.carbs,
|
||||
color: .carbsColor,
|
||||
label: "Carbs",
|
||||
unit: "g",
|
||||
size: 68
|
||||
)
|
||||
MacroRing(
|
||||
current: viewModel.totalFat,
|
||||
goal: viewModel.goal.fat,
|
||||
color: .fatColor,
|
||||
label: "Fat",
|
||||
unit: "g",
|
||||
size: 68
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.04), radius: 8, y: 4)
|
||||
}
|
||||
|
||||
private var quickActionsCard: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text("Quick Actions")
|
||||
.font(.headline)
|
||||
.foregroundStyle(Color.text1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
quickActionButton(icon: "plus.circle.fill", label: "Log Food", color: .accentEmerald)
|
||||
quickActionButton(icon: "doc.text.fill", label: "Templates", color: .carbsColor)
|
||||
quickActionButton(icon: "clock.fill", label: "History", color: .accentWarm)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.04), radius: 8, y: 4)
|
||||
}
|
||||
|
||||
private func macroStat(_ label: String, value: Int, unit: String) -> some View {
|
||||
VStack(spacing: 2) {
|
||||
Text("\(value)")
|
||||
.font(.system(.title3, design: .rounded, weight: .bold))
|
||||
.foregroundStyle(Color.text1)
|
||||
Text("\(label)")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Color.text4)
|
||||
}
|
||||
}
|
||||
|
||||
private func quickActionButton(icon: String, label: String, color: Color) -> some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(color)
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Color.text2)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(color.opacity(0.06))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor @Observable
|
||||
final class HomeViewModel {
|
||||
var todayEntries: [FoodEntry] = []
|
||||
var goal: DailyGoal = .defaultGoal
|
||||
var isLoading = true
|
||||
var errorMessage: String?
|
||||
|
||||
private let repo = FitnessRepository.shared
|
||||
|
||||
var totalCalories: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.calories }
|
||||
}
|
||||
|
||||
var totalProtein: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.protein }
|
||||
}
|
||||
|
||||
var totalCarbs: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.carbs }
|
||||
}
|
||||
|
||||
var totalFat: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.fat }
|
||||
}
|
||||
|
||||
var entryCount: Int {
|
||||
todayEntries.count
|
||||
}
|
||||
|
||||
func load() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
let today = Date().apiDateString
|
||||
|
||||
do {
|
||||
async let entriesTask = repo.entries(for: today, forceRefresh: true)
|
||||
async let goalsTask = repo.goals(for: today)
|
||||
|
||||
todayEntries = try await entriesTask
|
||||
goal = try await goalsTask
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user