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:
Millian Lamiaux
2026-04-21 22:47:47 +02:00
parent 2413bc0356
commit 877f836f19
2 changed files with 38 additions and 62 deletions

View File

@@ -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)