package ui import ( "fmt" "strings" "time" "github.com/charmbracelet/lipgloss" "skyrama-tui/internal/model" "skyrama-tui/internal/style" ) // AirportView renders the full airport overview panel. // Uses a single panel with two sections to avoid nested-panel line-count overhead. func AirportView(airport *model.Airport, country string, width int) string { if airport == nil { return lipgloss.JoinVertical(lipgloss.Left, style.PanelHeader.Render("🛫 Airport"), "", style.Muted.Render("No airport found for your account."), style.Muted.Render("Ask an admin to create one, or use the db_manager tool."), ) } barW := 22 fuelBar := style.ResourceBar( int(airport.CurrentFuel), atLeast1(int(airport.MaxFuelCapacity)), barW, style.ColorFuel) fuelLine := row( style.FuelChip.Width(14).Render("⛽ Fuel"), fuelBar, style.Muted.Render(fmt.Sprintf(" %d / %d", airport.CurrentFuel, airport.MaxFuelCapacity)), style.Muted.Render(fmt.Sprintf(" +%d/min", airport.FuelGenerationRate)), ) paxBar := style.ResourceBar( int(airport.CurrentPassengers), atLeast1(int(airport.MaxPassengerCapacity)), barW, style.ColorPassenger) paxLine := row( style.PassengerChip.Width(14).Render("👤 Passengers"), paxBar, style.Muted.Render(fmt.Sprintf(" %d / %d", airport.CurrentPassengers, airport.MaxPassengerCapacity)), style.Muted.Render(fmt.Sprintf(" +%d/min", airport.PassengerGenerationRate)), ) cashLine := style.CashChip.Render(fmt.Sprintf("💰 Cash $%s", formatCash(airport.Cash))) xpNeeded := model.XPToNextLevel(airport.PlayerLevel) xpCurrent := model.XPIntoLevel(airport.PlayerLevel, airport.ExperiencePoints) xpBar := style.ResourceBar(xpCurrent, atLeast1(xpNeeded), barW, style.ColorGreen) xpLine := row( style.Bold.Width(14).Render("✨ XP"), xpBar, style.Muted.Render(fmt.Sprintf(" %d / %d", xpCurrent, xpNeeded)), ) avail := style.StatusIdle.Render("Open ✓") if !airport.IsAvailable { avail = style.StatusMaint.Render("Closed ✗") } sinceSec := time.Since(airport.ResourcesLastUpdatedAt) syncStr := "just now" if sinceSec >= time.Second { syncStr = sinceSec.Round(time.Second).String() + " ago" } // Single panel containing both sections — avoids double-border line overhead. panel := style.Panel.Width(width).Render(lipgloss.JoinVertical(lipgloss.Left, style.PanelHeader.Render("Resources"), fuelLine, paxLine, cashLine, xpLine, "", style.PanelHeader.Render("Airport Info"), kv("Name", airport.Name), kv("Country", country), kv("Level", fmt.Sprintf("Lv.%d", airport.PlayerLevel)), kv("Status", avail), kv("Last sync", syncStr), )) // 1-line header to keep total view height minimal. header := lipgloss.JoinHorizontal(lipgloss.Top, style.Bold.Foreground(style.ColorSky).Width(width/2).Render("🛫 "+airport.Name), style.Muted.Width(width/2).Align(lipgloss.Right). Render(fmt.Sprintf("Lv.%d 📍 %s", airport.PlayerLevel, country)), ) return lipgloss.JoinVertical(lipgloss.Left, header, panel) } // ─── Helpers ────────────────────────────────────────────────────────────────── func row(parts ...string) string { return lipgloss.JoinHorizontal(lipgloss.Top, parts...) } func kv(key, val string) string { return lipgloss.JoinHorizontal(lipgloss.Top, style.Muted.Width(14).Render(key+":"), val, ) } func formatCash(v uint64) string { s := fmt.Sprintf("%d", v) var b strings.Builder for i, ch := range s { if i > 0 && (len(s)-i)%3 == 0 { b.WriteRune(',') } b.WriteRune(ch) } return b.String() } // atLeast1 prevents division-by-zero in progress bar calculations. func atLeast1(n int) int { if n <= 0 { return 1 } return n }