Files
termdoku/internal/ui/profile.go

288 lines
7.8 KiB
Go

package ui
import (
"fmt"
"strings"
"termdoku/internal/database"
"termdoku/internal/stats"
"termdoku/internal/theme"
"github.com/charmbracelet/lipgloss"
)
// viewProfile displays a simple profile landing page with navigation options.
func (a App) viewProfile() string {
adaptiveColors := theme.NewAdaptiveColors(a.th)
gradientColors := adaptiveColors.GetGradientColors()
profileGrad := gradientColors["banner"]
title := gradientText("User Profile", profileGrad[0], profileGrad[1])
var content strings.Builder
content.WriteString(title + "\n\n")
content.WriteString(a.renderProfileSummary())
content.WriteString("\n\n")
instructionStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color(a.th.Palette.Accent)).
Bold(true)
content.WriteString(instructionStyle.Render("Navigation:") + "\n")
content.WriteString(a.styles.Status.Render(" Press 's' to view detailed profile data (Stats, Achievements, Leaderboard)"))
content.WriteString("\n")
content.WriteString(a.styles.Status.Render(" Press 'd' to view database info"))
content.WriteString("\n")
content.WriteString(a.styles.Status.Render(" Press 'm' or Enter to return to menu"))
panel := a.styles.Panel.Render(content.String())
if a.width > 0 && a.height > 0 {
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
}
return a.styles.App.Render(panel)
}
// viewProfileSubmenu displays a dropdown menu for selecting profile data type.
func (a App) viewProfileSubmenu() string {
adaptiveColors := theme.NewAdaptiveColors(a.th)
gradientColors := adaptiveColors.GetGradientColors()
profileGrad := gradientColors["banner"]
title := gradientText(">> Select Profile Data", profileGrad[0], profileGrad[1])
var content strings.Builder
content.WriteString(title + "\n\n")
var items []string
accentColors := adaptiveColors.GetAccentColors()
selectedStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color(accentColors["selected"])).
Bold(true)
for i, name := range a.profileMenuItems {
if i == a.profileSelectedIdx {
prefix := "▶ "
label := prefix + name
items = append(items, selectedStyle.Render(label))
} else {
prefix := " "
label := prefix + name
items = append(items, a.styles.MenuItem.Render(label))
}
}
menuContent := strings.Join(items, "\n")
menuStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color(profileGrad[0])).
Padding(1, 2).
MarginTop(1).
MarginBottom(1)
content.WriteString(menuStyle.Render(menuContent))
content.WriteString("\n\n")
content.WriteString(a.styles.Status.Render("Use ↑/↓ or k/j to navigate, Enter to select"))
content.WriteString("\n")
content.WriteString(a.styles.Status.Render("Press 'm' or Esc to return to profile"))
panel := a.styles.Panel.Render(content.String())
if a.width > 0 && a.height > 0 {
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
}
return a.styles.App.Render(panel)
}
// renderProfileSummary renders the user summary card.
func (a App) renderProfileSummary() string {
var sb strings.Builder
totalGames := a.stats.TotalGames
completed := a.stats.CompletedGames
winRate := 0.0
if totalGames > 0 {
winRate = float64(completed) / float64(totalGames) * 100
}
rank := a.calculateRank(completed)
rankEmoji := a.getRankEmoji(rank)
summaryStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color(a.th.Palette.Accent)).
Padding(1, 2).
MarginTop(1)
summaryContent := fmt.Sprintf(
"%s Rank: %s"+
"\n>> Total Games: %d"+
"\n>> Completed: %d"+
"\n>> Win Rate: %.1f%%"+
"\n>> Current Streak: %d days"+
"\n>> Best Streak: %d days",
rankEmoji, rank,
totalGames,
completed,
winRate,
a.stats.CurrentStreak,
a.stats.BestStreak,
)
sb.WriteString(summaryStyle.Render(summaryContent))
return sb.String()
}
// calculateRank determines user rank based on completed games.
func (a App) calculateRank(completed int) string {
switch {
case completed >= 1000:
return "Grandmaster"
case completed >= 500:
return "Master"
case completed >= 250:
return "Expert"
case completed >= 100:
return "Advanced"
case completed >= 50:
return "Intermediate"
case completed >= 25:
return "Apprentice"
case completed >= 10:
return "Novice"
case completed >= 1:
return "Beginner"
default:
return "Newcomer"
}
}
// getRankEmoji returns an emoji for the rank.
func (a App) getRankEmoji(rank string) string {
switch rank {
case "Grandmaster":
return "👑"
case "Master":
return "🎖️"
case "Expert":
return "🏅"
case "Advanced":
return "⭐"
case "Intermediate":
return "🌟"
case "Apprentice":
return "✨"
case "Novice":
return "🔰"
case "Beginner":
return "🌱"
default:
return "👤"
}
}
// viewDatabaseInfo displays detailed database information.
func (a App) viewDatabaseInfo() string {
adaptiveColors := theme.NewAdaptiveColors(a.th)
gradientColors := adaptiveColors.GetGradientColors()
dbGrad := gradientColors["banner"]
title := gradientText("Database Info", dbGrad[0], dbGrad[1])
var content strings.Builder
content.WriteString(title + "\n\n")
db, err := database.Open()
if err != nil {
content.WriteString(a.styles.Status.Render(fmt.Sprintf("Error opening database: %v\n", err)))
} else {
defer db.Close()
dbStats, err := db.GetStats()
if err != nil {
content.WriteString(a.styles.Status.Render(fmt.Sprintf("Error reading stats: %v\n", err)))
} else {
statsStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color(a.th.Palette.Accent)).
Padding(1, 2)
statsContent := fmt.Sprintf(
"Total Games: %d"+
"\n>> Completed Games: %d"+
"\n>> Current Streak: %d"+
"\n>> Best Streak: %d"+
"\n>> Hints Used: %d"+
"\n>> Last Played: %s",
dbStats.TotalGames,
dbStats.CompletedGames,
dbStats.CurrentStreak,
dbStats.BestStreak,
dbStats.HintsUsed,
dbStats.LastPlayedDate,
)
content.WriteString(statsStyle.Render(statsContent))
}
content.WriteString("\n\n")
headerStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(a.th.Palette.Accent)).
Render("Achievements")
content.WriteString(headerStyle + "\n")
achievements, err := db.GetAchievements()
if err != nil {
content.WriteString(a.styles.Status.Render(fmt.Sprintf("Error reading achievements: %v\n", err)))
} else {
unlockedCount := 0
for _, ach := range achievements {
if ach.Unlocked {
unlockedCount++
}
}
content.WriteString(a.styles.Status.Render(fmt.Sprintf("Unlocked: %d/%d\n", unlockedCount, len(achievements))))
}
content.WriteString("\n")
headerStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(a.th.Palette.Accent)).
Render("Recent Games")
content.WriteString(headerStyle + "\n")
recentGames, err := db.GetRecentGames(5)
if err != nil {
content.WriteString(a.styles.Status.Render(fmt.Sprintf("Error reading games: %v\n", err)))
} else {
if len(recentGames) == 0 {
content.WriteString(a.styles.Status.Render("No games in database\n"))
} else {
for _, game := range recentGames {
statusIcon := "✅"
if !game.Completed {
statusIcon = "❌"
}
timeStr := stats.FormatTime(game.TimeSeconds)
line := fmt.Sprintf("%s %s - %s", statusIcon, game.Difficulty, timeStr)
if game.IsDaily {
line += " 📅"
}
content.WriteString(a.styles.Status.Render(line + "\n"))
}
}
}
}
content.WriteString("\n" + a.styles.Status.Render("Press 'm' or Enter to return to menu"))
panel := a.styles.Panel.Render(content.String())
if a.width > 0 && a.height > 0 {
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
}
return a.styles.App.Render(panel)
}