Replace double chip filters with segmented control + dropdown menu in ProgramsTab
Swap the two horizontal FilterChip scroll rows with a native segmented Picker for body zone and a toolbar Menu for difficulty level. Fix zone values to match Supabase enum (upper-body, lower-body, full-body). Remove unused FilterChip struct.
This commit is contained in:
@@ -543,6 +543,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"All" : {
|
||||
|
||||
},
|
||||
"Any challenges?" : {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ struct ProgramsTab: View {
|
||||
@State private var selectedProgram: WorkoutProgram? = nil
|
||||
@State private var searchText = ""
|
||||
|
||||
private var zones = ["upper", "lower", "full"]
|
||||
private var zones = ["upper-body", "lower-body", "full-body"]
|
||||
private var levels = ["Beginner", "Intermediate", "Advanced"]
|
||||
|
||||
private var filtered: [WorkoutProgram] {
|
||||
@@ -23,47 +23,25 @@ struct ProgramsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Label for the level toolbar button.
|
||||
private var levelMenuLabel: String {
|
||||
selectedLevel ?? "All Levels"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
// ── Zone Filter ───────────────────────────────
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
FilterChip(label: "All", isSelected: selectedZone == nil) {
|
||||
selectedZone = nil
|
||||
}
|
||||
ForEach(zones, id: \.self) { zone in
|
||||
FilterChip(
|
||||
label: zone.capitalized == "Full" ? "Full Body" : zone.capitalized,
|
||||
isSelected: selectedZone == zone,
|
||||
color: Theme.zoneColor(zone)
|
||||
) {
|
||||
selectedZone = selectedZone == zone ? nil : zone
|
||||
}
|
||||
}
|
||||
// ── Zone Segmented Control ────────────────────
|
||||
Picker("Body Zone", selection: $selectedZone) {
|
||||
Text("All").tag(String?.none)
|
||||
ForEach(zones, id: \.self) { zone in
|
||||
Text(zone.replacingOccurrences(of: "-", with: " ").capitalized)
|
||||
.tag(Optional(zone))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
// ── Level Filter ──────────────────────────────
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
FilterChip(label: "All Levels", isSelected: selectedLevel == nil) {
|
||||
selectedLevel = nil
|
||||
}
|
||||
ForEach(levels, id: \.self) { level in
|
||||
FilterChip(
|
||||
label: level,
|
||||
isSelected: selectedLevel == level,
|
||||
color: Theme.levelColor(level)
|
||||
) {
|
||||
selectedLevel = selectedLevel == level ? nil : level
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal)
|
||||
|
||||
// ── Program Grid ──────────────────────────────
|
||||
if vm.isLoading {
|
||||
@@ -92,6 +70,27 @@ struct ProgramsTab: View {
|
||||
.navigationTitle("Programs")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.searchable(text: $searchText, prompt: "Search workouts...")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
Button {
|
||||
selectedLevel = nil
|
||||
} label: {
|
||||
Label("All Levels", systemImage: selectedLevel == nil ? "checkmark" : "")
|
||||
}
|
||||
Divider()
|
||||
ForEach(levels, id: \.self) { level in
|
||||
Button {
|
||||
selectedLevel = selectedLevel == level ? nil : level
|
||||
} label: {
|
||||
Label(level, systemImage: selectedLevel == level ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(levelMenuLabel, systemImage: "line.3.horizontal.decrease.circle\(selectedLevel != nil ? ".fill" : "")")
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { await vm.loadPrograms() }
|
||||
.refreshable { await vm.refresh() }
|
||||
.sheet(item: $selectedProgram) { program in
|
||||
@@ -101,32 +100,6 @@ struct ProgramsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct FilterChip: View {
|
||||
let label: String
|
||||
let isSelected: Bool
|
||||
var color: Color = .primary
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text(label)
|
||||
.font(.subheadline.weight(isSelected ? .semibold : .regular))
|
||||
.foregroundStyle(isSelected ? .white : .primary)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.background {
|
||||
if isSelected {
|
||||
Capsule().fill(color == .primary ? Theme.brand : color)
|
||||
} else {
|
||||
Capsule().fill(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.animation(.spring(duration: 0.25), value: isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProgramsTab()
|
||||
.modelContainer(TabataGoSchema.previewContainer)
|
||||
|
||||
Reference in New Issue
Block a user