a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add profile editing UI in SSH (press 'e' in profile view)

+197 -37
+8
internal/storage/users.go
··· 96 96 return err 97 97 } 98 98 99 + func UpdateUserProfile(username, name, bio, link string) error { 100 + _, err := DB.Exec( 101 + "UPDATE users SET name = ?, bio = ?, link = ? WHERE username = ?", 102 + name, bio, link, username, 103 + ) 104 + return err 105 + } 106 + 99 107 func GetAllUsers() ([]User, error) { 100 108 rows, err := DB.Query( 101 109 `SELECT id, username, name, bio, link, public_key, created_at, last_login_at
+189 -37
internal/tui/model.go
··· 18 18 viewHome viewMode = iota 19 19 viewLeaderboard 20 20 viewProfile 21 + viewEditProfile 22 + ) 23 + 24 + type profileField int 25 + 26 + const ( 27 + fieldName profileField = iota 28 + fieldBio 29 + fieldLink 21 30 ) 22 31 23 32 var titleStyle = lipgloss.NewStyle(). ··· 27 36 MarginBottom(1) 28 37 29 38 type model struct { 30 - username string 31 - width int 32 - height int 33 - submissions []storage.Submission 34 - leaderboard []storage.LeaderboardEntry 35 - matches []storage.MatchResult 36 - externalURL string 37 - sshPort string 38 - currentView viewMode 39 + username string 40 + width int 41 + height int 42 + submissions []storage.Submission 43 + leaderboard []storage.LeaderboardEntry 44 + matches []storage.MatchResult 45 + externalURL string 46 + sshPort string 47 + currentView viewMode 48 + user *storage.User 49 + editingField profileField 50 + nameInput string 51 + bioInput string 52 + linkInput string 53 + saveMessage string 39 54 } 40 55 41 56 func InitialModel(username string, width, height int) model { ··· 56 71 sshPort = "2222" 57 72 } 58 73 74 + // Load user profile 75 + user, _ := storage.GetUserByUsername(username) 76 + 59 77 return model{ 60 - username: username, 61 - width: width, 62 - height: height, 63 - submissions: []storage.Submission{}, 64 - leaderboard: []storage.LeaderboardEntry{}, 65 - externalURL: externalURL, 66 - sshPort: sshPort, 67 - currentView: viewHome, 78 + username: username, 79 + width: width, 80 + height: height, 81 + submissions: []storage.Submission{}, 82 + leaderboard: []storage.LeaderboardEntry{}, 83 + externalURL: externalURL, 84 + sshPort: sshPort, 85 + currentView: viewHome, 86 + user: user, 87 + editingField: fieldName, 68 88 } 69 89 } 70 90 ··· 75 95 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 76 96 switch msg := msg.(type) { 77 97 case tea.KeyMsg: 98 + if m.currentView == viewEditProfile { 99 + return m.updateEditProfile(msg) 100 + } 101 + 78 102 switch msg.String() { 79 103 case "ctrl+c", "q": 80 104 return m, tea.Quit ··· 84 108 m.currentView = viewLeaderboard 85 109 case "p", "3": 86 110 m.currentView = viewProfile 111 + case "e": 112 + if m.currentView == viewProfile { 113 + m.currentView = viewEditProfile 114 + // Initialize inputs with current values 115 + if m.user != nil { 116 + m.nameInput = m.user.Name 117 + m.bioInput = m.user.Bio 118 + m.linkInput = m.user.Link 119 + } 120 + m.editingField = fieldName 121 + m.saveMessage = "" 122 + } 87 123 } 88 124 case tea.WindowSizeMsg: 89 125 m.width = msg.Width ··· 100 136 return m, nil 101 137 } 102 138 139 + func (m model) updateEditProfile(msg tea.KeyMsg) (tea.Model, tea.Cmd) { 140 + switch msg.String() { 141 + case "ctrl+c", "q", "esc": 142 + m.currentView = viewProfile 143 + m.saveMessage = "" 144 + return m, nil 145 + case "tab", "down": 146 + m.editingField = (m.editingField + 1) % 3 147 + case "shift+tab", "up": 148 + m.editingField = (m.editingField + 2) % 3 149 + case "enter": 150 + // Save profile 151 + err := storage.UpdateUserProfile(m.username, m.nameInput, m.bioInput, m.linkInput) 152 + if err != nil { 153 + m.saveMessage = "Error saving profile" 154 + } else { 155 + m.saveMessage = "Profile saved!" 156 + // Reload user 157 + m.user, _ = storage.GetUserByUsername(m.username) 158 + m.currentView = viewProfile 159 + } 160 + return m, nil 161 + case "backspace": 162 + switch m.editingField { 163 + case fieldName: 164 + if len(m.nameInput) > 0 { 165 + m.nameInput = m.nameInput[:len(m.nameInput)-1] 166 + } 167 + case fieldBio: 168 + if len(m.bioInput) > 0 { 169 + m.bioInput = m.bioInput[:len(m.bioInput)-1] 170 + } 171 + case fieldLink: 172 + if len(m.linkInput) > 0 { 173 + m.linkInput = m.linkInput[:len(m.linkInput)-1] 174 + } 175 + } 176 + default: 177 + // Add character to current field 178 + if len(msg.String()) == 1 { 179 + switch m.editingField { 180 + case fieldName: 181 + if len(m.nameInput) < 50 { 182 + m.nameInput += msg.String() 183 + } 184 + case fieldBio: 185 + if len(m.bioInput) < 200 { 186 + m.bioInput += msg.String() 187 + } 188 + case fieldLink: 189 + if len(m.linkInput) < 100 { 190 + m.linkInput += msg.String() 191 + } 192 + } 193 + } 194 + } 195 + return m, nil 196 + } 197 + 103 198 104 199 105 200 func (m model) View() string { ··· 108 203 title := titleStyle.Render("🚢 Battleship Arena") 109 204 b.WriteString(title + "\n") 110 205 111 - // Navigation tabs 112 - tabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 113 - activeTabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 114 - 115 - tabs := []string{"[h] Home", "[l] Leaderboard", "[p] Profile"} 116 - for i, tab := range tabs { 117 - if viewMode(i) == m.currentView { 118 - b.WriteString(activeTabStyle.Render(tab)) 119 - } else { 120 - b.WriteString(tabStyle.Render(tab)) 121 - } 122 - if i < len(tabs)-1 { 123 - b.WriteString(" ") 206 + // Skip tabs if in edit mode 207 + if m.currentView != viewEditProfile { 208 + // Navigation tabs 209 + tabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 210 + activeTabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 211 + 212 + tabs := []string{"[h] Home", "[l] Leaderboard", "[p] Profile"} 213 + for i, tab := range tabs { 214 + if viewMode(i) == m.currentView { 215 + b.WriteString(activeTabStyle.Render(tab)) 216 + } else { 217 + b.WriteString(tabStyle.Render(tab)) 218 + } 219 + if i < len(tabs)-1 { 220 + b.WriteString(" ") 221 + } 124 222 } 223 + b.WriteString("\n\n") 125 224 } 126 - b.WriteString("\n\n") 127 225 128 226 // Render content based on current view 129 227 switch m.currentView { ··· 133 231 b.WriteString(m.renderLeaderboardView()) 134 232 case viewProfile: 135 233 b.WriteString(m.renderProfile()) 234 + case viewEditProfile: 235 + b.WriteString(m.renderEditProfile()) 136 236 } 137 237 138 - b.WriteString("\n\nPress q to quit") 238 + if m.currentView != viewEditProfile { 239 + b.WriteString("\n\nPress q to quit") 240 + } 139 241 140 242 return b.String() 141 243 } ··· 170 272 func (m model) renderProfile() string { 171 273 var b strings.Builder 172 274 173 - b.WriteString(fmt.Sprintf("Profile: %s\n\n", m.username)) 275 + if m.user == nil { 276 + return "Loading profile..." 277 + } 278 + 279 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("👤 Profile") + "\n\n") 280 + 281 + // Show user info 282 + labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 283 + b.WriteString(labelStyle.Render("Username: ") + m.user.Username + "\n") 284 + b.WriteString(labelStyle.Render("Name: ") + m.user.Name + "\n") 285 + b.WriteString(labelStyle.Render("Bio: ") + m.user.Bio + "\n") 286 + b.WriteString(labelStyle.Render("Link: ") + m.user.Link + "\n\n") 287 + 288 + hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")) 289 + b.WriteString(hintStyle.Render("Press 'e' to edit profile") + "\n\n") 174 290 175 291 // Show user stats from submissions 176 292 if len(m.submissions) > 0 { ··· 178 294 b.WriteString("\n") 179 295 } 180 296 181 - // Show recent matches involving this user 182 - if len(m.matches) > 0 { 183 - b.WriteString("\nRecent Matches:\n") 184 - b.WriteString(renderMatches(m.matches, m.username)) 297 + return b.String() 298 + } 299 + 300 + func (m model) renderEditProfile() string { 301 + var b strings.Builder 302 + 303 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("✏️ Edit Profile") + "\n\n") 304 + 305 + activeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 306 + inactiveStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 307 + 308 + // Name field 309 + if m.editingField == fieldName { 310 + b.WriteString(activeStyle.Render("► Name: ") + m.nameInput + "█\n") 311 + } else { 312 + b.WriteString(inactiveStyle.Render(" Name: ") + m.nameInput + "\n") 185 313 } 314 + 315 + // Bio field 316 + if m.editingField == fieldBio { 317 + b.WriteString(activeStyle.Render("► Bio: ") + m.bioInput + "█\n") 318 + } else { 319 + b.WriteString(inactiveStyle.Render(" Bio: ") + m.bioInput + "\n") 320 + } 321 + 322 + // Link field 323 + if m.editingField == fieldLink { 324 + b.WriteString(activeStyle.Render("► Link: ") + m.linkInput + "█\n") 325 + } else { 326 + b.WriteString(inactiveStyle.Render(" Link: ") + m.linkInput + "\n") 327 + } 328 + 329 + b.WriteString("\n") 330 + 331 + if m.saveMessage != "" { 332 + msgStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("green")) 333 + b.WriteString(msgStyle.Render(m.saveMessage) + "\n\n") 334 + } 335 + 336 + hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 337 + b.WriteString(hintStyle.Render("Tab/↑↓: Navigate fields | Enter: Save | Esc: Cancel")) 186 338 187 339 return b.String() 188 340 }