189 lines
6.4 KiB
Swift
189 lines
6.4 KiB
Swift
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))
|
|
}
|
|
}
|