Redesign FeaturedProgramCard and reorder Home sections

- Replace external pill badges (level + FREE) with immersive overlay card:
  level tag pinned top-right with ultraThinMaterial blur, FREE shown inline
  with checkmark.seal.fill icon, dark scrim for text legibility
- Remove card drop shadow
- Move Browse by Zone section above Featured in Home tab scroll order
This commit is contained in:
Millian Lamiaux
2026-04-21 23:18:15 +02:00
parent 9f15ae2d79
commit e28bebea79

View File

@@ -53,6 +53,22 @@ struct HomeTab: View {
}
.padding(.horizontal)
// Body Zone Grid
VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "Browse by Zone", subtitle: "Target specific muscle groups")
.padding(.horizontal)
VStack(spacing: 12) {
ForEach(vm.availableZones, id: \.self) { zone in
NavigationLink(destination: BodyZoneView(zone: zone)) {
ZoneCard(zone: zone)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
}
// Featured Workouts
if !vm.featuredPrograms.isEmpty {
VStack(alignment: .leading, spacing: 12) {
@@ -71,22 +87,6 @@ struct HomeTab: View {
}
}
// Body Zone Grid
VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "Browse by Zone", subtitle: "Target specific muscle groups")
.padding(.horizontal)
VStack(spacing: 12) {
ForEach(vm.availableZones, id: \.self) { zone in
NavigationLink(destination: BodyZoneView(zone: zone)) {
ZoneCard(zone: zone)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
}
// Loading / Error State
if vm.isLoading {
ProgressView()
@@ -148,42 +148,55 @@ struct FeaturedProgramCard: View {
let program: WorkoutProgram
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header gradient area
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Theme.zoneGradient(program.bodyZone))
.frame(width: 220, height: 110)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: 4) {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Theme.zoneGradient(program.bodyZone))
.frame(width: 220, height: 170)
.overlay {
ZStack(alignment: .topTrailing) {
// Bottom scrim for text legibility
LinearGradient(
colors: [.clear, .black.opacity(0.55)],
startPoint: .center,
endPoint: .bottom
)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
// Level tag top right
Text(program.level.capitalized)
.font(.caption2.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, 9)
.padding(.vertical, 5)
.background(.ultraThinMaterial.opacity(0.85))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.padding(12)
// Bottom content
VStack(alignment: .leading, spacing: 6) {
Spacer()
Text(program.titleEn)
.font(.headline.weight(.bold))
.foregroundStyle(.white)
.lineLimit(2)
HStack(spacing: 6) {
Label("\(program.estimatedDuration)m", systemImage: "clock")
Label("\(program.estimatedCalories) kcal", systemImage: "flame")
}
.font(.caption.weight(.medium))
.foregroundStyle(.white.opacity(0.85))
}
.padding(12)
}
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
HStack {
LevelBadge(level: program.level)
Spacer()
if program.isFree {
Text("FREE")
.font(.caption2.weight(.bold))
.foregroundStyle(Theme.success)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Theme.success.opacity(0.15))
.clipShape(Capsule())
HStack(spacing: 10) {
Label("\(program.estimatedDuration)m", systemImage: "clock.fill")
Label("\(program.estimatedCalories) kcal", systemImage: "flame.fill")
if program.isFree {
Label("Free", systemImage: "checkmark.seal.fill")
.foregroundStyle(Theme.success)
}
}
.font(.caption.weight(.semibold))
.foregroundStyle(.white.opacity(0.9))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
}
}
}
.frame(width: 220)
}
}