| struct WatchSitesView: View {
@EnvironmentObject var store: WatchEntryStore
// MARK: - Data
/// Sites grouped by priority (5 → 1 → 0), then alphabetical within each group.
private var byPriority: [(priority: Int, entries: [WatchEntry])] {
[5, 4, 3, 2, 1, 0].compactMap { p in
let group = store.entries
.filter { $0.entryMode == .place && !$0.beenThere && $0.rating == p }
.sorted { $0.playName.lowercased() < $1.playName.lowercased() }
return group.isEmpty ? nil : (priority: p, entries: group)
}
}
private var beenSites: [WatchEntry] {
store.entries
.filter { $0.entryMode == .place && $0.beenThere }
.sorted { ($0.starRating, $0.playName) > ($1.starRating, $1.playName) }
}
private var hasAny: Bool { !byPriority.isEmpty || !beenSites.isEmpty }
// MARK: - Body
var body: some View {
Group {
if !store.isLoaded {
ProgressView()
} else if !hasAny {
VStack(spacing: 8) {
Image(systemName: "mappin.and.ellipse")
.font(.system(size: 28))
.foregroundColor(.orange)
Text("No sites yet")
.font(.caption)
.foregroundColor(.secondary)
}
} else {
List {
// ── To Visit (grouped by priority) ──────────────────
if !byPriority.isEmpty {
Section("To Visit") {
ForEach(Array(byPriority.enumerated()), id: \.element.priority) { idx, group in
// Priority dots header row
if group.priority > 0 {
HStack(spacing: 3) {
ForEach(1...5, id: \.self) { dot in
Image(systemName: dot <= group.priority ? "circle.fill" : "circle")
.font(.system(size: 8))
.foregroundColor(dot <= group.priority ? .white : .gray.opacity(0.3))
}
}
.frame(height: 14)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: idx == 0 ? 2 : 4, leading: 8, bottom: 0, trailing: 8))
}
ForEach(group.entries) { entry in
NavigationLink(destination: WatchEntryDetailView(entry: entry)) {
SiteRow(entry: entry, showAsBeen: false)
}
}
}
}
}
// ── Been ────────────────────────────────────────────
if !beenSites.isEmpty {
Section("Been") {
ForEach(beenSites) { entry in
NavigationLink(destination: WatchEntryDetailView(entry: entry)) {
SiteRow(entry: entry, showAsBeen: true)
}
}
}
}
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, 0)
}
}
.navigationTitle("Sites")
.navigationBarTitleDisplayMode(.inline)
}
} | `WatchSitesView` struct | Defines the `WatchSitesView` struct. Conforms to View. |
| private struct SiteRow: View {
let entry: WatchEntry
let showAsBeen: Bool
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(entry.playName)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(lavender)
.lineLimit(2)
if !entry.neighborhood.isEmpty {
Text(entry.neighborhood)
.font(.system(size: 11))
.foregroundColor(.white.opacity(0.75))
.lineLimit(1)
} else if !entry.cuisine.isEmpty {
Text(entry.cuisine)
.font(.system(size: 11))
.foregroundColor(.white.opacity(0.75))
.lineLimit(1)
}
if showAsBeen && entry.starRating > 0 {
HStack(spacing: 2) {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= entry.starRating ? "star.fill" : "star")
.font(.system(size: 7))
.foregroundColor(star <= entry.starRating ? .yellow : .gray.opacity(0.4))
}
}
}
}
.padding(.vertical, 2)
}
} | `SiteRow` struct | Defines the `SiteRow` struct. Conforms to View. |