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:
@@ -0,0 +1,72 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingView: View {
|
||||
var message: String = "Loading..."
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.tint(Color.accentWarm)
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.text3)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.canvas)
|
||||
}
|
||||
}
|
||||
|
||||
struct ErrorBanner: View {
|
||||
let message: String
|
||||
var onRetry: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(Color.error)
|
||||
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.text2)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let onRetry {
|
||||
Button("Retry") {
|
||||
onRetry()
|
||||
}
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(Color.accentWarm)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color.error.opacity(0.06))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyStateView: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(Color.text4)
|
||||
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(Color.text2)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.text3)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(40)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MacroBar: View {
|
||||
let label: String
|
||||
let current: Double
|
||||
let goal: Double
|
||||
let color: Color
|
||||
var showGrams: Bool = true
|
||||
|
||||
private var progress: Double {
|
||||
guard goal > 0 else { return 0 }
|
||||
return min(current / goal, 1.0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Color.text3)
|
||||
Spacer()
|
||||
if showGrams {
|
||||
Text("\(Int(current))g / \(Int(goal))g")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.text3)
|
||||
} else {
|
||||
Text("\(Int(current)) / \(Int(goal))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.text3)
|
||||
}
|
||||
}
|
||||
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(color.opacity(0.12))
|
||||
.frame(height: 6)
|
||||
|
||||
Capsule()
|
||||
.fill(color)
|
||||
.frame(width: geo.size.width * progress, height: 6)
|
||||
.animation(.easeOut(duration: 0.5), value: progress)
|
||||
}
|
||||
}
|
||||
.frame(height: 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MacroBarCompact: View {
|
||||
let current: Double
|
||||
let goal: Double
|
||||
let color: Color
|
||||
|
||||
private var progress: Double {
|
||||
guard goal > 0 else { return 0 }
|
||||
return min(current / goal, 1.0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(color.opacity(0.12))
|
||||
|
||||
Capsule()
|
||||
.fill(color)
|
||||
.frame(width: geo.size.width * progress)
|
||||
.animation(.easeOut(duration: 0.5), value: progress)
|
||||
}
|
||||
}
|
||||
.frame(height: 4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MacroRing: View {
|
||||
let current: Double
|
||||
let goal: Double
|
||||
let color: Color
|
||||
let label: String
|
||||
let unit: String
|
||||
var size: CGFloat = 72
|
||||
var lineWidth: CGFloat = 7
|
||||
|
||||
private var progress: Double {
|
||||
guard goal > 0 else { return 0 }
|
||||
return min(current / goal, 1.0)
|
||||
}
|
||||
|
||||
private var remaining: Double {
|
||||
max(goal - current, 0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(color.opacity(0.12), lineWidth: lineWidth)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(
|
||||
color,
|
||||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeOut(duration: 0.5), value: progress)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text("\(Int(remaining))")
|
||||
.font(.system(size: size * 0.22, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Color.text1)
|
||||
Text("left")
|
||||
.font(.system(size: size * 0.13, weight: .medium))
|
||||
.foregroundStyle(Color.text4)
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Color.text3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MacroRingLarge: View {
|
||||
let current: Double
|
||||
let goal: Double
|
||||
let color: Color
|
||||
var size: CGFloat = 120
|
||||
var lineWidth: CGFloat = 10
|
||||
|
||||
private var progress: Double {
|
||||
guard goal > 0 else { return 0 }
|
||||
return min(current / goal, 1.0)
|
||||
}
|
||||
|
||||
private var remaining: Double {
|
||||
max(goal - current, 0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(color.opacity(0.12), lineWidth: lineWidth)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(
|
||||
color,
|
||||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeOut(duration: 0.5), value: progress)
|
||||
|
||||
VStack(spacing: 2) {
|
||||
Text("\(Int(remaining))")
|
||||
.font(.system(size: size * 0.26, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Color.text1)
|
||||
Text("remaining")
|
||||
.font(.system(size: size * 0.11, weight: .medium))
|
||||
.foregroundStyle(Color.text4)
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
// Warm palette matching web app
|
||||
static let canvas = Color(hex: "F5EFE6")
|
||||
static let surface = Color.white
|
||||
static let surfaceSecondary = Color(hex: "F4F4F5")
|
||||
static let cardBackground = Color.white
|
||||
static let cardSecondary = Color(hex: "F4F4F5")
|
||||
|
||||
static let text1 = Color(hex: "18181B")
|
||||
static let text2 = Color(hex: "3F3F46")
|
||||
static let text3 = Color(hex: "71717A")
|
||||
static let text4 = Color(hex: "A1A1AA")
|
||||
|
||||
// Accent — warm amber/brown
|
||||
static let accentWarm = Color(hex: "8B6914")
|
||||
static let accentWarmBg = Color(hex: "FEF7E6")
|
||||
|
||||
// Emerald accent from web
|
||||
static let accentEmerald = Color(hex: "059669")
|
||||
static let accentEmeraldBg = Color(hex: "ECFDF5")
|
||||
|
||||
// Semantic
|
||||
static let success = Color(hex: "059669")
|
||||
static let error = Color(hex: "DC2626")
|
||||
static let warning = Color(hex: "D97706")
|
||||
|
||||
// Macro colors
|
||||
static let caloriesColor = Color(hex: "8B6914")
|
||||
static let proteinColor = Color(hex: "059669")
|
||||
static let carbsColor = Color(hex: "3B82F6")
|
||||
static let fatColor = Color(hex: "F59E0B")
|
||||
static let sugarColor = Color(hex: "EC4899")
|
||||
static let fiberColor = Color(hex: "8B5CF6")
|
||||
|
||||
// Meal colors
|
||||
static let breakfast = Color(hex: "F59E0B")
|
||||
static let lunch = Color(hex: "059669")
|
||||
static let dinner = Color(hex: "3B82F6")
|
||||
static let snack = Color(hex: "8B5CF6")
|
||||
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3:
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6:
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8:
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
|
||||
static func mealColor(for meal: String) -> Color {
|
||||
switch meal.lowercased() {
|
||||
case "breakfast": return .breakfast
|
||||
case "lunch": return .lunch
|
||||
case "dinner": return .dinner
|
||||
case "snack": return .snack
|
||||
default: return .text3
|
||||
}
|
||||
}
|
||||
|
||||
static func mealIcon(for meal: String) -> String {
|
||||
switch meal.lowercased() {
|
||||
case "breakfast": return "sunrise.fill"
|
||||
case "lunch": return "sun.max.fill"
|
||||
case "dinner": return "moon.fill"
|
||||
case "snack": return "leaf.fill"
|
||||
default: return "fork.knife"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
extension Date {
|
||||
/// Format as yyyy-MM-dd for API calls
|
||||
var apiDateString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
/// Display format: "Mon, Apr 2"
|
||||
var displayString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEE, MMM d"
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
/// Full display: "Monday, April 2, 2026"
|
||||
var fullDisplayString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .full
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
/// Short display: "Apr 2"
|
||||
var shortDisplayString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d"
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
var isToday: Bool {
|
||||
Calendar.current.isDateInToday(self)
|
||||
}
|
||||
|
||||
var isYesterday: Bool {
|
||||
Calendar.current.isDateInYesterday(self)
|
||||
}
|
||||
|
||||
func adding(days: Int) -> Date {
|
||||
Calendar.current.date(byAdding: .day, value: days, to: self) ?? self
|
||||
}
|
||||
|
||||
static func from(apiString: String) -> Date? {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
return formatter.date(from: apiString)
|
||||
}
|
||||
|
||||
/// Returns a label like "Today", "Yesterday", or the display string
|
||||
var relativeLabel: String {
|
||||
if isToday { return "Today" }
|
||||
if isYesterday { return "Yesterday" }
|
||||
return displayString
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user