(Feat): Initial Commit, Termdoku
This commit is contained in:
287
internal/ui/profile.go
Normal file
287
internal/ui/profile.go
Normal file
@@ -0,0 +1,287 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user