feat: add user avatar with dicebear dylan

This commit is contained in:
2026-05-07 13:42:21 +02:00
parent 83e480b7b0
commit b5083035f5
6 changed files with 60 additions and 16 deletions

View File

@@ -6,3 +6,4 @@ dist
*.db-wal *.db-wal
.DS_Store .DS_Store
.git .git
static

View File

@@ -14,19 +14,24 @@ import (
) )
func main() { func main() {
if len(os.Args) != 3 {
log.Fatalf("Usage: go run cmd/user/main.go <username> <password>")
}
username := os.Args[1]
password := os.Args[2]
dbConn, err := db.Open(db.GetDBFile()) dbConn, err := db.Open(db.GetDBFile())
if err != nil { if err != nil {
log.Fatalf("failed to open db: %v", err) log.Fatalf("failed to open db: %v", err)
} }
defer dbConn.Close() defer dbConn.Close()
if len(os.Args) == 2 && os.Args[1] == "update-avatar" {
updateAvatars(dbConn)
return
}
if len(os.Args) != 3 {
log.Fatalf("Usage: go run cmd/user/main.go <username> <password>\n go run cmd/user/main.go update-avatar")
}
username := os.Args[1]
password := os.Args[2]
var existingID string var existingID string
err = dbConn.QueryRow("SELECT id FROM user WHERE username = ?", username).Scan(&existingID) err = dbConn.QueryRow("SELECT id FROM user WHERE username = ?", username).Scan(&existingID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
@@ -64,10 +69,40 @@ func main() {
} }
id := uuid.New().String() id := uuid.New().String()
_, err = dbConn.Exec("INSERT INTO user (id, username, password_hash) VALUES (?, ?, ?)", id, username, string(hash)) avatarURL := fmt.Sprintf("https://api.dicebear.com/9.x/dylan/svg?seed=%s", username)
_, err = dbConn.Exec("INSERT INTO user (id, username, password_hash, avatar_url) VALUES (?, ?, ?, ?)", id, username, string(hash), avatarURL)
if err != nil { if err != nil {
log.Fatalf("failed to create user: %v", err) log.Fatalf("failed to create user: %v", err)
} }
fmt.Printf("User '%s' was created successfully!\n", username) fmt.Printf("User '%s' was created successfully!\n", username)
} }
func updateAvatars(dbConn *sql.DB) {
rows, err := dbConn.Query("SELECT id, username FROM user")
if err != nil {
log.Fatalf("failed to fetch users: %v", err)
}
defer rows.Close()
count := 0
for rows.Next() {
var id, username string
if err := rows.Scan(&id, &username); err != nil {
log.Fatalf("failed to scan user: %v", err)
}
avatarURL := fmt.Sprintf("https://api.dicebear.com/9.x/dylan/svg?seed=%s", username)
_, err := dbConn.Exec("UPDATE user SET avatar_url = ? WHERE id = ?", avatarURL, id)
if err != nil {
log.Fatalf("failed to update avatar for %s: %v", username, err)
}
count++
}
if err := rows.Err(); err != nil {
log.Fatalf("iteration error: %v", err)
}
fmt.Printf("Updated avatars for %d user(s)\n", count)
}

View File

@@ -64,9 +64,10 @@ type Session struct {
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
CreatedAt time.Time `json:"created_at"` AvatarURL string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
} }
type WatchListEntry struct { type WatchListEntry struct {

View File

@@ -469,7 +469,7 @@ func (q *Queries) GetUpcomingSeasons(ctx context.Context, userID string) ([]GetU
} }
const getUser = `-- name: GetUser :one const getUser = `-- name: GetUser :one
SELECT id, username, password_hash, created_at FROM user WHERE id = ? LIMIT 1 SELECT id, username, password_hash, avatar_url, created_at FROM user WHERE id = ? LIMIT 1
` `
func (q *Queries) GetUser(ctx context.Context, id string) (User, error) { func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
@@ -479,13 +479,14 @@ func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
&i.ID, &i.ID,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
&i.AvatarURL,
&i.CreatedAt, &i.CreatedAt,
) )
return i, err return i, err
} }
const getUserByUsername = `-- name: GetUserByUsername :one const getUserByUsername = `-- name: GetUserByUsername :one
SELECT id, username, password_hash, created_at FROM user WHERE username = ? LIMIT 1 SELECT id, username, password_hash, avatar_url, created_at FROM user WHERE username = ? LIMIT 1
` `
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
@@ -495,6 +496,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
&i.ID, &i.ID,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
&i.AvatarURL,
&i.CreatedAt, &i.CreatedAt,
) )
return i, err return i, err

View File

@@ -0,0 +1,3 @@
ALTER TABLE user ADD COLUMN avatar_url TEXT NOT NULL DEFAULT '';
UPDATE user SET avatar_url = 'https://api.dicebear.com/9.x/dylan/svg?seed=' || username WHERE avatar_url = '';

View File

@@ -42,9 +42,11 @@
<div data-trigger class="cursor-pointer"> <div data-trigger class="cursor-pointer">
<button class="flex items-center gap-1 rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none"> <button class="flex items-center gap-1 rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none">
{{if .User}} {{if .User}}
<div class="bg-accent flex h-8 w-8 items-center justify-center overflow-hidden rounded-full text-sm font-semibold text-white"> <img
{{slice .User.Username 0 1}} src="{{.User.AvatarURL}}"
</div> alt="{{.User.Username}}"
class="h-8 w-8 rounded-full object-cover"
/>
{{else}} {{else}}
<div class="bg-accent flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold text-white"> <div class="bg-accent flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>