Clear demo data, namespace storage by user, add profile setup
- Remove all personal sampleTasks from TaskViewModel (tasks start empty) - Namespace UserDefaults keys by Apple ID for task and workout data isolation - Add ProfileSetupView for name capture after first Sign in with Apple - Add updateDisplayName() to AuthManager - Route to ProfileSetupView when user has no display name yet Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
||||||
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */; };
|
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */; };
|
||||||
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */; };
|
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */; };
|
||||||
|
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
|
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
|
||||||
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
||||||
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
||||||
|
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -97,6 +99,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */,
|
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */,
|
||||||
|
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */,
|
||||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
|
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
|
||||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
|
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
|
||||||
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
|
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
|
||||||
@@ -226,6 +229,7 @@
|
|||||||
files = (
|
files = (
|
||||||
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */,
|
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */,
|
||||||
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */,
|
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */,
|
||||||
|
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */,
|
||||||
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */,
|
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */,
|
||||||
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */,
|
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */,
|
||||||
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */,
|
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */,
|
||||||
|
|||||||
@@ -10,8 +10,14 @@ struct KisaniCalApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
ZStack {
|
ZStack {
|
||||||
if auth.isAuthenticated {
|
if auth.isAuthenticated {
|
||||||
|
let hasName = !(auth.currentUser?.displayName?.trimmingCharacters(in: .whitespaces).isEmpty ?? true)
|
||||||
|
if hasName {
|
||||||
ContentView()
|
ContentView()
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
} else {
|
||||||
|
ProfileSetupView()
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
AuthView()
|
AuthView()
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ final class AuthManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Profile
|
||||||
|
|
||||||
|
func updateDisplayName(_ name: String) {
|
||||||
|
guard var user = currentUser, !name.trimmingCharacters(in: .whitespaces).isEmpty else { return }
|
||||||
|
user.displayName = name.trimmingCharacters(in: .whitespaces)
|
||||||
|
persist(user)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Sign Out
|
// MARK: - Sign Out
|
||||||
|
|
||||||
func signOut() {
|
func signOut() {
|
||||||
|
|||||||
@@ -175,22 +175,66 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
@Published var schedule: [Int: UUID]
|
@Published var schedule: [Int: UUID]
|
||||||
@Published var activeProgramId: UUID
|
@Published var activeProgramId: UUID
|
||||||
@Published var activeSections: [WorkoutSection]
|
@Published var activeSections: [WorkoutSection]
|
||||||
@Published var streakDays: Int = 5
|
@Published var workoutDates: [String] = []
|
||||||
@Published var restTimerSeconds: Int = 60
|
@Published var restTimerSeconds: Int = 60
|
||||||
|
@Published var sessionStartDate: Date? = nil
|
||||||
|
|
||||||
|
var streakDays: Int {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let fmt = iso; let dateSet = Set(workoutDates)
|
||||||
|
var streak = 0
|
||||||
|
var check = cal.startOfDay(for: Date())
|
||||||
|
if !dateSet.contains(fmt.string(from: check)) {
|
||||||
|
check = cal.date(byAdding: .day, value: -1, to: check)!
|
||||||
|
}
|
||||||
|
while dateSet.contains(fmt.string(from: check)) {
|
||||||
|
streak += 1
|
||||||
|
check = cal.date(byAdding: .day, value: -1, to: check)!
|
||||||
|
}
|
||||||
|
return streak
|
||||||
|
}
|
||||||
|
|
||||||
|
private let iso: DateFormatter = {
|
||||||
|
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f
|
||||||
|
}()
|
||||||
@Published var showAddExercise: Bool = false
|
@Published var showAddExercise: Bool = false
|
||||||
@Published var addExerciseTargetSectionId: UUID?
|
@Published var addExerciseTargetSectionId: UUID?
|
||||||
|
|
||||||
private static let kPrograms = "kisani.workout.programs"
|
private let kPrograms: String
|
||||||
private static let kSchedule = "kisani.workout.schedule"
|
private let kSchedule: String
|
||||||
private static let kActivePid = "kisani.workout.activePid"
|
private let kActivePid: String
|
||||||
|
private let kWorkoutDates: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||||
|
self.kPrograms = "kisani.workout.programs.\(uid)"
|
||||||
|
self.kSchedule = "kisani.workout.schedule.\(uid)"
|
||||||
|
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
||||||
|
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
|
||||||
|
|
||||||
// ── Attempt to restore persisted state ──
|
// ── Attempt to restore persisted state ──
|
||||||
if let data = UserDefaults.standard.data(forKey: Self.kPrograms),
|
if let data = UserDefaults.standard.data(forKey: kPrograms),
|
||||||
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
||||||
!saved.isEmpty {
|
!saved.isEmpty {
|
||||||
|
|
||||||
let emojiMap = ["🔥": "🔄", "💥": "🏋️"]
|
// Migrate any legacy emoji → SF Symbol name
|
||||||
|
let emojiMap: [String: String] = [
|
||||||
|
"🏋️": "figure.strengthtraining.traditional",
|
||||||
|
"💪": "figure.strengthtraining.functional",
|
||||||
|
"🔄": "figure.arms.open",
|
||||||
|
"🦵": "figure.run",
|
||||||
|
"⚡": "bolt.fill",
|
||||||
|
"⚡️": "bolt.fill",
|
||||||
|
"🏃": "figure.run",
|
||||||
|
"🤸": "figure.yoga",
|
||||||
|
"🧘": "figure.yoga",
|
||||||
|
"🚴": "figure.cycling",
|
||||||
|
"🥊": "figure.boxing",
|
||||||
|
"🎯": "figure.core.training",
|
||||||
|
"🫀": "heart.fill",
|
||||||
|
"🔥": "bolt.fill",
|
||||||
|
"💥": "figure.highintensity.intervaltraining"
|
||||||
|
]
|
||||||
let needsMigration = saved.contains(where: { emojiMap[$0.emoji] != nil })
|
let needsMigration = saved.contains(where: { emojiMap[$0.emoji] != nil })
|
||||||
let migrated: [WorkoutProgram] = needsMigration
|
let migrated: [WorkoutProgram] = needsMigration
|
||||||
? saved.map { p in
|
? saved.map { p in
|
||||||
@@ -200,7 +244,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
: saved
|
: saved
|
||||||
|
|
||||||
var sched: [Int: UUID] = [:]
|
var sched: [Int: UUID] = [:]
|
||||||
if let dict = UserDefaults.standard.dictionary(forKey: Self.kSchedule) as? [String: String] {
|
if let dict = UserDefaults.standard.dictionary(forKey: kSchedule) as? [String: String] {
|
||||||
for (k, v) in dict {
|
for (k, v) in dict {
|
||||||
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
|
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
|
||||||
}
|
}
|
||||||
@@ -208,7 +252,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
schedule = sched
|
schedule = sched
|
||||||
|
|
||||||
var pid = saved[0].id
|
var pid = saved[0].id
|
||||||
if let str = UserDefaults.standard.string(forKey: Self.kActivePid),
|
if let str = UserDefaults.standard.string(forKey: kActivePid),
|
||||||
let uid = UUID(uuidString: str),
|
let uid = UUID(uuidString: str),
|
||||||
saved.contains(where: { $0.id == uid }) { pid = uid }
|
saved.contains(where: { $0.id == uid }) { pid = uid }
|
||||||
activeProgramId = pid
|
activeProgramId = pid
|
||||||
@@ -219,30 +263,17 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
if needsMigration { save() }
|
if needsMigration { save() }
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// ── First launch: seed with sample programs ──
|
// ── First launch: start blank so the user builds their own routine ──
|
||||||
let progs = WorkoutViewModel.samplePrograms()
|
let blank = WorkoutProgram(
|
||||||
programs = progs
|
name: "My Workout", emoji: "dumbbell.fill", colorIndex: 0,
|
||||||
|
sections: [WorkoutSection(name: "Main", exercises: [])]
|
||||||
var sched: [Int: UUID] = [:]
|
)
|
||||||
if progs.count >= 6 {
|
programs = [blank]
|
||||||
sched[2] = progs[0].id
|
schedule = [:]
|
||||||
sched[3] = progs[1].id
|
activeProgramId = blank.id
|
||||||
sched[4] = progs[2].id
|
activeSections = blank.sections
|
||||||
sched[5] = progs[3].id
|
|
||||||
sched[6] = progs[4].id
|
|
||||||
sched[7] = progs[5].id
|
|
||||||
}
|
|
||||||
schedule = sched
|
|
||||||
|
|
||||||
let today = Calendar.current.component(.weekday, from: Date())
|
|
||||||
if let pid = sched[today], progs.contains(where: { $0.id == pid }) {
|
|
||||||
activeProgramId = pid
|
|
||||||
activeSections = progs.first { $0.id == pid }!.sections
|
|
||||||
} else {
|
|
||||||
activeProgramId = progs[0].id
|
|
||||||
activeSections = progs[0].sections
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
workoutDates = UserDefaults.standard.stringArray(forKey: kWorkoutDates) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
@@ -252,11 +283,39 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
||||||
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
|
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
|
||||||
UserDefaults.standard.set(activeProgramId.uuidString, forKey: Self.kActivePid)
|
UserDefaults.standard.set(activeProgramId.uuidString, forKey: Self.kActivePid)
|
||||||
|
UserDefaults.standard.set(workoutDates, forKey: Self.kWorkoutDates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logWorkoutCompleted(on date: Date = Date()) {
|
||||||
|
let key = iso.string(from: date)
|
||||||
|
guard !workoutDates.contains(key) else { return }
|
||||||
|
workoutDates.append(key)
|
||||||
|
save()
|
||||||
|
let start = sessionStartDate ?? date.addingTimeInterval(-3600)
|
||||||
|
Task { await HealthKitManager.shared.saveWorkout(start: start, end: date) }
|
||||||
|
sessionStartDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedStreak(days: Int) {
|
||||||
|
let cal = Calendar.current
|
||||||
|
var newDates = Set(workoutDates)
|
||||||
|
for i in 0..<days {
|
||||||
|
let d = cal.date(byAdding: .day, value: -i, to: cal.startOfDay(for: Date()))!
|
||||||
|
newDates.insert(iso.string(from: d))
|
||||||
|
}
|
||||||
|
workoutDates = Array(newDates)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setScheduleForDays(_ weekdays: Set<Int>) {
|
||||||
|
schedule = [:]
|
||||||
|
for day in weekdays { schedule[day] = activeProgramId }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
|
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
|
||||||
var workoutTitle: String { activeProgram?.name ?? "Workout" }
|
var workoutTitle: String { activeProgram?.name ?? "Workout" }
|
||||||
var workoutEmoji: String { activeProgram?.emoji ?? "💪" }
|
var workoutEmoji: String { activeProgram?.emoji ?? "dumbbell.fill" }
|
||||||
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
|
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
|
||||||
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
|
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
|
||||||
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
|
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
|
||||||
@@ -345,7 +404,11 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
|
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
|
||||||
else { return }
|
else { return }
|
||||||
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
|
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
|
||||||
|
if sessionStartDate == nil, activeSections[si].exercises[ei].sets[setIdx].isDone {
|
||||||
|
sessionStartDate = Date()
|
||||||
|
}
|
||||||
syncBack()
|
syncBack()
|
||||||
|
if doneSets == totalSets && totalSets > 0 { logWorkoutCompleted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSet(sectionId: UUID, to exerciseId: UUID) {
|
func addSet(sectionId: UUID, to exerciseId: UUID) {
|
||||||
@@ -453,7 +516,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
let lb: Double = 0.453592
|
let lb: Double = 0.453592
|
||||||
|
|
||||||
// ── Monday: Chest & Triceps ──────────────────────────────────────────
|
// ── Monday: Chest & Triceps ──────────────────────────────────────────
|
||||||
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "🏋️", colorIndex: 0, sections: [
|
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "figure.strengthtraining.traditional", colorIndex: 0, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Chest", exercises: [
|
WorkoutSection(name: "Chest", exercises: [
|
||||||
// Incline first — 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
|
// Incline first — 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
|
||||||
@@ -499,7 +562,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// ── Tuesday: Back & Biceps ───────────────────────────────────────────
|
// ── Tuesday: Back & Biceps ───────────────────────────────────────────
|
||||||
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "💪", colorIndex: 1, sections: [
|
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "figure.strengthtraining.functional", colorIndex: 1, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Back", exercises: [
|
WorkoutSection(name: "Back", exercises: [
|
||||||
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
|
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
|
||||||
@@ -538,7 +601,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// ── Wednesday: Shoulders, Triceps & Biceps ───────────────────────────
|
// ── Wednesday: Shoulders, Triceps & Biceps ───────────────────────────
|
||||||
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "🔄", colorIndex: 2, sections: [
|
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "figure.arms.open", colorIndex: 2, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Shoulders", exercises: [
|
WorkoutSection(name: "Shoulders", exercises: [
|
||||||
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
|
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
|
||||||
@@ -557,7 +620,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// ── Thursday: Legs & Chest ───────────────────────────────────────────
|
// ── Thursday: Legs & Chest ───────────────────────────────────────────
|
||||||
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "🦵", colorIndex: 3, sections: [
|
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "figure.run", colorIndex: 3, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Legs", exercises: [
|
WorkoutSection(name: "Legs", exercises: [
|
||||||
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
|
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
|
||||||
@@ -574,7 +637,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// ── Friday: Full Body ────────────────────────────────────────────────
|
// ── Friday: Full Body ────────────────────────────────────────────────
|
||||||
let fri = WorkoutProgram(name: "Full Body", emoji: "⚡", colorIndex: 0, sections: [
|
let fri = WorkoutProgram(name: "Full Body", emoji: "bolt.fill", colorIndex: 0, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Upper Body", exercises: [
|
WorkoutSection(name: "Upper Body", exercises: [
|
||||||
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||||
@@ -591,7 +654,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// ── Saturday: Full Body ──────────────────────────────────────────────
|
// ── Saturday: Full Body ──────────────────────────────────────────────
|
||||||
let sat = WorkoutProgram(name: "Full Body", emoji: "🏋️", colorIndex: 1, sections: [
|
let sat = WorkoutProgram(name: "Full Body", emoji: "figure.highintensity.intervaltraining", colorIndex: 1, sections: [
|
||||||
warmUp,
|
warmUp,
|
||||||
WorkoutSection(name: "Upper Body", exercises: [
|
WorkoutSection(name: "Upper Body", exercises: [
|
||||||
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||||
|
|||||||
@@ -58,20 +58,22 @@ enum TaskColor: String, Codable {
|
|||||||
class TaskViewModel: ObservableObject {
|
class TaskViewModel: ObservableObject {
|
||||||
@Published var tasks: [TaskItem]
|
@Published var tasks: [TaskItem]
|
||||||
|
|
||||||
private static let persistKey = "kisani.tasks.v2"
|
private let persistKey: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if let data = UserDefaults.standard.data(forKey: Self.persistKey),
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||||
|
self.persistKey = "kisani.tasks.v2.\(uid)"
|
||||||
|
if let data = UserDefaults.standard.data(forKey: persistKey),
|
||||||
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
||||||
tasks = saved
|
tasks = saved
|
||||||
} else {
|
} else {
|
||||||
tasks = sampleTasks
|
tasks = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
if let data = try? JSONEncoder().encode(tasks) {
|
if let data = try? JSONEncoder().encode(tasks) {
|
||||||
UserDefaults.standard.set(data, forKey: Self.persistKey)
|
UserDefaults.standard.set(data, forKey: persistKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,37 +185,3 @@ class TaskViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sample Data
|
|
||||||
private let sampleTasks: [TaskItem] = [
|
|
||||||
.init(title: "Azure exam preparation",
|
|
||||||
dueDate: cal.date(byAdding: .day, value: -7, to: Date()),
|
|
||||||
quadrant: .urgent, category: .custom, taskColor: .orange),
|
|
||||||
.init(title: "Leona's Birthday",
|
|
||||||
dueDate: md(6, 17), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
|
|
||||||
.init(title: "Provoc.ug Domain Expiry",
|
|
||||||
dueDate: md(7, 3), quadrant: .urgent, category: .domain, taskColor: .blue),
|
|
||||||
.init(title: "Farouk's Birthday",
|
|
||||||
dueDate: md(11, 8), quadrant: .delegate_, category: .birthday, taskColor: .green, isRecurring: true),
|
|
||||||
.init(title: "Enoch's Birthday",
|
|
||||||
dueDate: md(2, 23, 2027), quadrant: .schedule, category: .annual, taskColor: .yellow, isRecurring: true),
|
|
||||||
.init(title: "Dad's birthday",
|
|
||||||
dueDate: md(6, 8), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
|
|
||||||
.init(title: "Moms bd",
|
|
||||||
dueDate: md(2, 15, 2027), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
|
|
||||||
.init(title: "Amooti's birthday",
|
|
||||||
dueDate: md(6, 26), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
|
|
||||||
.init(title: "Provoc hosting",
|
|
||||||
dueDate: md(11, 25, 2027), quadrant: .delegate_, category: .domain, taskColor: .blue),
|
|
||||||
.init(title: "DV lottery preps",
|
|
||||||
dueDate: md(9, 30, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
|
||||||
.init(title: "Catch up Umutoni",
|
|
||||||
dueDate: md(11, 15, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
|
||||||
.init(title: "Siraj wedding preps",
|
|
||||||
dueDate: md(8, 24, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
|
||||||
]
|
|
||||||
|
|
||||||
private let cal = Calendar.current
|
|
||||||
private func md(_ month: Int, _ day: Int, _ year: Int = 2026) -> Date {
|
|
||||||
var c = DateComponents(); c.year = year; c.month = month; c.day = day
|
|
||||||
return cal.date(from: c) ?? Date()
|
|
||||||
}
|
|
||||||
|
|||||||
85
KisaniCal/Views/ProfileSetupView.swift
Normal file
85
KisaniCal/Views/ProfileSetupView.swift
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileSetupView: View {
|
||||||
|
@ObservedObject private var auth = AuthManager.shared
|
||||||
|
@Environment(\.colorScheme) private var cs
|
||||||
|
@State private var name = ""
|
||||||
|
@FocusState private var focused: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
AppColors.background(cs).ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.fill(AppColors.accentSoft)
|
||||||
|
.frame(width: 88, height: 88)
|
||||||
|
Image(systemName: "person.fill")
|
||||||
|
.font(.system(size: 36, weight: .light))
|
||||||
|
.foregroundColor(AppColors.accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("What's your name?")
|
||||||
|
.font(AppFonts.sans(26, weight: .bold))
|
||||||
|
.foregroundColor(AppColors.text(cs))
|
||||||
|
Text("This is how you'll appear in the app.")
|
||||||
|
.font(AppFonts.sans(15))
|
||||||
|
.foregroundColor(AppColors.text2(cs))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
TextField("Your name", text: $name)
|
||||||
|
.font(AppFonts.sans(17))
|
||||||
|
.foregroundColor(AppColors.text(cs))
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 14)
|
||||||
|
.fill(Color(uiColor: cs == .dark ? .secondarySystemBackground : .systemGray6))
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
.focused($focused)
|
||||||
|
.submitLabel(.done)
|
||||||
|
.onSubmit { save() }
|
||||||
|
|
||||||
|
Button(action: save) {
|
||||||
|
Text("Continue")
|
||||||
|
.font(AppFonts.sans(17, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 54)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 14)
|
||||||
|
.fill(name.trimmingCharacters(in: .whitespaces).isEmpty
|
||||||
|
? AppColors.accent.opacity(0.4)
|
||||||
|
: AppColors.accent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
|
||||||
|
Button("Skip for now") {
|
||||||
|
auth.updateDisplayName("User")
|
||||||
|
}
|
||||||
|
.font(AppFonts.sans(14))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
}
|
||||||
|
.padding(.bottom, 56)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear { focused = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() {
|
||||||
|
auth.updateDisplayName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user