Sort toggle (cheapest/closest) + RAG value rating per station with TOP badge
This commit is contained in:
+84
-11
@@ -7,6 +7,7 @@ struct ContentView: View {
|
||||
|
||||
@State private var stations: [FuelStation] = []
|
||||
@State private var selectedFuel: FuelType = FuelStore.loadSelectedFuel()
|
||||
@State private var sortMode: SortMode = FuelStore.loadSortMode()
|
||||
@State private var location: Coordinate? = {
|
||||
if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) }
|
||||
return nil
|
||||
@@ -15,10 +16,24 @@ struct ContentView: View {
|
||||
@State private var statusMessage = ""
|
||||
@State private var locationManager = LocationManager()
|
||||
|
||||
private var cheapestPrice: Double? {
|
||||
stations.compactMap { $0.prices[selectedFuel] }.min()
|
||||
}
|
||||
|
||||
private var sortedStations: [FuelStation] {
|
||||
stations
|
||||
.filter { $0.prices[selectedFuel] != nil }
|
||||
.sorted { lhs, rhs in
|
||||
let available = stations.filter { $0.prices[selectedFuel] != nil }
|
||||
switch sortMode {
|
||||
case .closest:
|
||||
guard let location else { return available.sorted { $0.prices[selectedFuel]! < $1.prices[selectedFuel]! } }
|
||||
return available.sorted { lhs, rhs in
|
||||
// Closest first; price only breaks ties.
|
||||
let lDist = lhs.distanceKM(to: location.lat, lng2: location.lng)
|
||||
let rDist = rhs.distanceKM(to: location.lat, lng2: location.lng)
|
||||
if lDist != rDist { return lDist < rDist }
|
||||
return lhs.prices[selectedFuel]! < rhs.prices[selectedFuel]!
|
||||
}
|
||||
case .cheapest:
|
||||
return available.sorted { lhs, rhs in
|
||||
// Cheapest first; distance only breaks ties.
|
||||
let lPrice = lhs.prices[selectedFuel]!
|
||||
let rPrice = rhs.prices[selectedFuel]!
|
||||
@@ -27,17 +42,30 @@ struct ContentView: View {
|
||||
return lhs.distanceKM(to: location.lat, lng2: location.lng) <
|
||||
rhs.distanceKM(to: location.lat, lng2: location.lng)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
Text("Cheapest \(selectedFuel.displayName) near you. Tap a station for directions.")
|
||||
Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) — tap a station for directions. 🟢 great value · 🟡 okay · 🔴 pricey")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("Sort by") {
|
||||
Picker("Sort by", selection: $sortMode) {
|
||||
ForEach(SortMode.allCases) { mode in
|
||||
Text(mode.displayName).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.onChange(of: sortMode) { _, newValue in
|
||||
FuelStore.saveSortMode(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Fuel type") {
|
||||
Picker("Fuel type", selection: $selectedFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
@@ -61,8 +89,14 @@ struct ContentView: View {
|
||||
Text("No \(selectedFuel.displayName) stations found.")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(sortedStations) { station in
|
||||
StationRow(station: station, fuel: selectedFuel, location: location)
|
||||
ForEach(Array(sortedStations.enumerated()), id: \.element.id) { index, station in
|
||||
StationRow(
|
||||
station: station,
|
||||
fuel: selectedFuel,
|
||||
location: location,
|
||||
cheapestPrice: cheapestPrice,
|
||||
isTopResult: index == 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,12 +181,40 @@ struct StationRow: View {
|
||||
let station: FuelStation
|
||||
let fuel: FuelType
|
||||
let location: Coordinate?
|
||||
let cheapestPrice: Double?
|
||||
let isTopResult: Bool
|
||||
|
||||
private var ragColor: Color {
|
||||
guard let price = station.prices[fuel], let cheapestPrice else { return .gray }
|
||||
switch RAGRating.rating(price: price, cheapest: cheapestPrice) {
|
||||
case .green: return .green
|
||||
case .amber: return .orange
|
||||
case .red: return .red
|
||||
}
|
||||
}
|
||||
|
||||
private var deltaText: String? {
|
||||
guard let price = station.prices[fuel], let cheapestPrice else { return nil }
|
||||
let delta = price - cheapestPrice
|
||||
if delta <= 0.05 { return "best" }
|
||||
return String(format: "+%.1fp", delta)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(station.name)
|
||||
.font(.headline)
|
||||
HStack(spacing: 6) {
|
||||
Text(station.name)
|
||||
.font(.headline)
|
||||
if isTopResult {
|
||||
Text("TOP")
|
||||
.font(.caption2.bold())
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 1)
|
||||
.background(Capsule().fill(.blue.opacity(0.15)))
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
Text("\(station.address), \(station.postcode)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -164,9 +226,20 @@ struct StationRow: View {
|
||||
}
|
||||
Spacer()
|
||||
if let price = station.prices[fuel] {
|
||||
Text(String(format: "%.1fp", price))
|
||||
.font(.title3.bold())
|
||||
.foregroundStyle(.green)
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
HStack(spacing: 5) {
|
||||
Circle()
|
||||
.fill(ragColor)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(String(format: "%.1fp", price))
|
||||
.font(.title3.bold())
|
||||
}
|
||||
if let deltaText {
|
||||
Text(deltaText)
|
||||
.font(.caption2.bold())
|
||||
.foregroundStyle(ragColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
Image(systemName: "arrow.triangle.turn.up.right.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Reference in New Issue
Block a user