288 lines
7.8 KiB
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)
|
|
}
|