176 lines
6.9 KiB
Swift
176 lines
6.9 KiB
Swift
import SwiftUI
|
|
|
|
struct FeedView: View {
|
|
@EnvironmentObject private var localization: LocalizationStore
|
|
@EnvironmentObject private var analytics: HermesAnalyticsClient
|
|
@EnvironmentObject private var repository: HermesRepository
|
|
|
|
let onWatchPreview: () -> Void = {}
|
|
let onRetry: () -> Void
|
|
|
|
var body: some View {
|
|
TimelineView(.periodic(from: Date(), by: 1)) { _ in
|
|
let round = repository.currentRound
|
|
let now = repository.serverNow()
|
|
let bannerMessage = hermesUserFacingErrorMessage(
|
|
localization: localization,
|
|
localeCode: localization.localeCode,
|
|
error: repository.errorCause
|
|
)
|
|
|
|
VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) {
|
|
HermesSectionHeader(
|
|
title: localization.string(for: "feed.title"),
|
|
subtitle: localization.string(for: "feed.subtitle")
|
|
)
|
|
|
|
if round == nil {
|
|
if let bannerMessage {
|
|
feedErrorState(
|
|
message: bannerMessage,
|
|
retryText: localization.string(for: "common.retry"),
|
|
onRetry: onRetry
|
|
)
|
|
} else {
|
|
feedLoadingState(
|
|
title: localization.string(for: "common.loading"),
|
|
subtitle: localization.string(for: "feed.subtitle")
|
|
)
|
|
}
|
|
} else {
|
|
if let bannerMessage {
|
|
feedBanner(message: bannerMessage)
|
|
}
|
|
|
|
heroCard(round: round)
|
|
|
|
HStack(spacing: 12) {
|
|
HermesMetricPill(
|
|
label: localization.string(for: "feed.lock_label"),
|
|
value: round.map { Self.countdownText(for: $0.event.lockAt.timeIntervalSince(now)) } ?? "--:--"
|
|
)
|
|
HermesMetricPill(
|
|
label: localization.string(for: "feed.odds_label"),
|
|
value: round.map { Self.formatOdds($0) } ?? "--"
|
|
)
|
|
}
|
|
|
|
Button {
|
|
analytics.track("next_round_requested", attributes: ["screen_name": "feed"])
|
|
analytics.track("cta_pressed", attributes: ["screen_name": "feed", "action": "watch_preview"])
|
|
onWatchPreview()
|
|
} label: {
|
|
Text(localization.string(for: "feed.cta"))
|
|
}
|
|
.buttonStyle(HermesPrimaryButtonStyle())
|
|
.disabled(round == nil)
|
|
}
|
|
}
|
|
.onAppear {
|
|
analytics.track("feed_viewed", attributes: ["screen_name": "feed"])
|
|
analytics.track("screen_viewed", attributes: ["screen_name": "feed"])
|
|
}
|
|
}
|
|
.hermesCard(elevated: true)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func heroCard(round: HermesRound?) -> some View {
|
|
ZStack(alignment: .bottomLeading) {
|
|
RoundedRectangle(cornerRadius: HermesTheme.cornerRadius, style: .continuous)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [HermesTheme.surfaceElevated, HermesTheme.background],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(height: 220)
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(round.map { localizedEventTitle($0) } ?? localization.string(for: "feed.hero_title"))
|
|
.font(.title2.weight(.bold))
|
|
.foregroundStyle(HermesTheme.textPrimary)
|
|
|
|
Text(localization.string(for: "feed.hero_subtitle"))
|
|
.font(.callout)
|
|
.foregroundStyle(HermesTheme.textSecondary)
|
|
.frame(maxWidth: 260, alignment: .leading)
|
|
}
|
|
.padding(HermesTheme.contentPadding)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func feedLoadingState(title: String, subtitle: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(title)
|
|
.font(.title2.weight(.bold))
|
|
.foregroundStyle(HermesTheme.textPrimary)
|
|
Text(subtitle)
|
|
.font(.callout)
|
|
.foregroundStyle(HermesTheme.textSecondary)
|
|
}
|
|
.padding(HermesTheme.contentPadding)
|
|
.frame(maxWidth: .infinity, minHeight: 220, alignment: .center)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: HermesTheme.cornerRadius, style: .continuous)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [HermesTheme.surfaceElevated, HermesTheme.background],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func feedErrorState(message: String, retryText: String, onRetry: @escaping () -> Void) -> some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(message)
|
|
.font(.callout)
|
|
.foregroundStyle(HermesTheme.warning)
|
|
|
|
Button {
|
|
onRetry()
|
|
} label: {
|
|
Text(retryText)
|
|
}
|
|
.buttonStyle(HermesSecondaryButtonStyle())
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func feedBanner(message: String) -> some View {
|
|
Text(message)
|
|
.font(.callout)
|
|
.foregroundStyle(HermesTheme.warning)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 10)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(HermesTheme.warning.opacity(0.12))
|
|
.clipShape(RoundedRectangle(cornerRadius: HermesTheme.insetRadius, style: .continuous))
|
|
}
|
|
|
|
private func localizedEventTitle(_ round: HermesRound) -> String {
|
|
localization.localeCode == "sv" ? round.event.titleSv : round.event.titleEn
|
|
}
|
|
|
|
private static func countdownText(for remaining: TimeInterval) -> String {
|
|
let totalSeconds = max(Int(remaining.rounded(.down)), 0)
|
|
let minutes = totalSeconds / 60
|
|
let seconds = totalSeconds % 60
|
|
return String(format: "%02d:%02d", minutes, seconds)
|
|
}
|
|
|
|
private static func formatOdds(_ round: HermesRound) -> String {
|
|
let oddsByOutcomeId = Dictionary(uniqueKeysWithValues: round.oddsVersion.odds.map { ($0.outcomeId, $0) })
|
|
return round.market.outcomes
|
|
.sorted(by: { $0.sortOrder < $1.sortOrder })
|
|
.compactMap { oddsByOutcomeId[$0.id]?.decimalOdds }
|
|
.map { String(format: "%.2f", $0) }
|
|
.joined(separator: " / ")
|
|
}
|
|
}
|