remove Expo project and all related files

Remove the entire Expo/React Native application: routes (app/), source
code (src/), assets, iOS native build, config plugins, StoreKit config,
npm dependencies, TypeScript/ESLint/Vitest configs, and Expo-specific
documentation. The repository now contains only: admin-web, supabase,
youtube-worker, tabatago-swift, docs, scripts, and CI/tooling configs.
This commit is contained in:
Millian Lamiaux
2026-04-21 21:55:00 +02:00
parent 8c90b73d90
commit 89cca25e22
285 changed files with 11212 additions and 44392 deletions

View File

@@ -0,0 +1,156 @@
import SwiftUI
import HealthKit
/// Mini activity/streak summary shown on the Watch when idle.
/// Displays today's Move/Exercise/Stand ring progress from HealthKit
/// and a streak count stored via App Group UserDefaults (shared with phone).
struct WatchActivityView: View {
@State private var moveProgress: Double = 0 // 0-1
@State private var exerciseProgress: Double = 0
@State private var standProgress: Double = 0
@State private var streak: Int = 0
@State private var isLoading = true
private let healthStore = HKHealthStore()
private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
var body: some View {
ScrollView {
VStack(spacing: 14) {
// Rings
Text("Today")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 12) {
RingView(progress: moveProgress,
color: Color(red: 1.0, green: 0.23, blue: 0.19),
icon: "flame.fill",
label: "Move")
RingView(progress: exerciseProgress,
color: .green,
icon: "figure.run",
label: "Exercise")
RingView(progress: standProgress,
color: Color(red: 0.04, green: 0.80, blue: 0.97),
icon: "figure.stand",
label: "Stand")
}
Divider()
// Streak
HStack {
Image(systemName: "bolt.fill")
.foregroundStyle(.orange)
.font(.system(size: 14))
Text("\(streak) day\(streak == 1 ? "" : "s")")
.font(.system(size: 14, weight: .bold, design: .rounded))
Spacer()
Text("streak")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 4)
.padding(.vertical, 8)
.redacted(reason: isLoading ? .placeholder : [])
}
.task { await loadData() }
}
// Data loading
private func loadData() async {
streak = sharedDefaults?.integer(forKey: "streak") ?? 0
guard HKHealthStore.isHealthDataAvailable() else {
isLoading = false
return
}
let typesToRead: Set<HKObjectType> = [
HKObjectType.activitySummaryType()
]
guard (try? await healthStore.requestAuthorization(toShare: [], read: typesToRead)) != nil else {
isLoading = false
return
}
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let predicate = HKQuery.predicateForActivitySummary(with: calendar.dateComponents(
[.era, .year, .month, .day], from: today))
let summaries = try? await withCheckedThrowingContinuation { (cont: CheckedContinuation<[HKActivitySummary], Error>) in
let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in
if let error { cont.resume(throwing: error) }
else { cont.resume(returning: summaries ?? []) }
}
healthStore.execute(query)
}
if let summary = summaries?.first {
let moveGoal = summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())
let exerciseGoal = summary.appleExerciseTimeGoal.doubleValue(for: .minute())
let standGoal = summary.appleStandHoursGoal.doubleValue(for: .count())
await MainActor.run {
moveProgress = moveGoal > 0
? summary.activeEnergyBurned.doubleValue(for: .kilocalorie()) / moveGoal
: 0
exerciseProgress = exerciseGoal > 0
? summary.appleExerciseTime.doubleValue(for: .minute()) / exerciseGoal
: 0
standProgress = standGoal > 0
? summary.appleStandHours.doubleValue(for: .count()) / standGoal
: 0
isLoading = false
}
} else {
await MainActor.run { isLoading = false }
}
}
}
// Sub-components
private struct RingView: View {
let progress: Double
let color: Color
let icon: String
let label: String
var body: some View {
VStack(spacing: 4) {
ZStack {
// Track
Circle()
.stroke(color.opacity(0.2), lineWidth: 5)
// Fill
Circle()
.trim(from: 0, to: min(progress, 1.0))
.stroke(color, style: StrokeStyle(lineWidth: 5, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.easeOut(duration: 0.6), value: progress)
// Icon
Image(systemName: icon)
.font(.system(size: 9, weight: .bold))
.foregroundStyle(color)
}
.frame(width: 36, height: 36)
Text(label)
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
}
}
#Preview {
WatchActivityView()
}

View File

@@ -0,0 +1,43 @@
import SwiftUI
/// Idle state waiting for a workout to start from the phone.
struct WatchIdleView: View {
@EnvironmentObject private var connectivity: WatchConnectivityManager
var body: some View {
VStack(spacing: 12) {
Image(systemName: "bolt.fill")
.font(.system(size: 36, weight: .bold))
.foregroundStyle(.orange)
Text("TabataGo")
.font(.system(size: 18, weight: .bold, design: .rounded))
Text("Start a workout\non your iPhone")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
if connectivity.isPhoneReachable {
HStack(spacing: 4) {
Circle()
.fill(.green)
.frame(width: 6, height: 6)
Text("Connected")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
} else {
HStack(spacing: 4) {
Circle()
.fill(.gray)
.frame(width: 6, height: 6)
Text("No phone")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}
}
.padding()
}
}

View File

@@ -0,0 +1,156 @@
import SwiftUI
/// Active workout player on Watch full-screen timer with phase, HR, calories.
struct WatchPlayerView: View {
@EnvironmentObject private var engine: WatchPlayerEngine
var body: some View {
ZStack {
// Phase color background
watchPhaseColor(engine.phase)
.opacity(0.18)
.ignoresSafeArea()
VStack(spacing: 4) {
// Phase label
Text(watchPhaseLabel(engine.phase))
.font(.system(size: 11, weight: .bold, design: .rounded))
.foregroundStyle(watchPhaseColor(engine.phase))
.kerning(1.5)
// Timer
Text("\(engine.timeRemaining)")
.font(.system(size: 52, weight: .black, design: .rounded))
.monospacedDigit()
.foregroundStyle(.white)
.contentTransition(.numericText(countsDown: true))
.animation(.spring(duration: 0.3), value: engine.timeRemaining)
// Exercise name
if let name = engine.currentExerciseName {
Text(name)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white.opacity(0.85))
.lineLimit(1)
.minimumScaleFactor(0.7)
}
// Round pips
RoundPips(
current: engine.currentRound,
total: min(engine.totalRoundsInBlock, 8)
)
.padding(.vertical, 4)
// Live metrics
HStack(spacing: 16) {
if engine.heartRate > 0 {
WatchMetric(
icon: "heart.fill",
value: "\(Int(engine.heartRate))",
color: .red
)
}
if engine.activeCalories > 0 {
WatchMetric(
icon: "flame.fill",
value: "\(Int(engine.activeCalories))",
color: .orange
)
}
}
// Pause / End controls
HStack(spacing: 12) {
Button {
engine.togglePause()
} label: {
Image(systemName: engine.isPaused ? "play.fill" : "pause.fill")
.font(.system(size: 14, weight: .bold))
.frame(width: 36, height: 36)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
.buttonStyle(.plain)
Button(role: .destructive) {
engine.endWorkout()
} label: {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .bold))
.frame(width: 36, height: 36)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 8)
}
}
private func watchPhaseColor(_ phase: WatchPhase) -> Color {
switch phase {
case .prep, .warmup: return .orange
case .work: return Color(red: 1.0, green: 0.42, blue: 0.21)
case .rest, .interBlockRest: return Color(red: 0.35, green: 0.78, blue: 0.98)
case .cooldown: return .cyan
case .complete: return .green
}
}
private func watchPhaseLabel(_ phase: WatchPhase) -> String {
switch phase {
case .prep: return "GET READY"
case .warmup: return "WARM UP"
case .work: return "WORK"
case .rest: return "REST"
case .interBlockRest: return "BREAK"
case .cooldown: return "COOL DOWN"
case .complete: return "DONE"
}
}
}
// Sub-components
struct RoundPips: View {
let current: Int
let total: Int
var body: some View {
HStack(spacing: 4) {
ForEach(1...max(total, 1), id: \.self) { i in
Capsule()
.fill(i < current ? Color.orange :
i == current ? .white : .white.opacity(0.25))
.frame(width: i == current ? 16 : 6, height: 5)
.animation(.spring(duration: 0.25), value: current)
}
}
}
}
struct WatchMetric: View {
let icon: String
let value: String
let color: Color
var body: some View {
HStack(spacing: 3) {
Image(systemName: icon)
.font(.system(size: 10))
.foregroundStyle(color)
Text(value)
.font(.system(size: 13, weight: .semibold, design: .rounded))
.monospacedDigit()
}
}
}
#Preview {
WatchPlayerView()
.environmentObject(WatchPlayerEngine())
.environmentObject(WatchConnectivityManager.shared)
}

View File

@@ -0,0 +1,17 @@
import SwiftUI
/// Root view: shows idle screen or active player depending on state.
struct WatchRootView: View {
@EnvironmentObject private var playerEngine: WatchPlayerEngine
var body: some View {
Group {
if playerEngine.isActive {
WatchPlayerView()
} else {
WatchIdleView()
}
}
.animation(.easeInOut(duration: 0.3), value: playerEngine.isActive)
}
}