A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

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

Add followers and following for a user on the friends page.

oscar345 10a925ad 18c92c61

+170 -19
+35 -12
internal/repo/db/user_follow.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 - "fmt" 7 6 8 7 "github.com/oscar345/keeptrack/internal/models" 9 8 "github.com/oscar345/keeptrack/internal/repo" ··· 21 20 return &UserFollowRepoDB{db: db} 22 21 } 23 22 24 - func (repo *UserFollowRepoDB) Follow(ctx context.Context, userID string, targetID string) error { 23 + func (repo *UserFollowRepoDB) Follow(ctx context.Context, userID int, targetID int) error { 25 24 values := map[string]any{"user_id": userID, "target_id": targetID} 26 25 if _, err := database.Insert(ctx, repo.db, "user_follows", values, database.WithIgnore()); err != nil { 27 26 return err ··· 29 28 return nil 30 29 } 31 30 32 - func (repo *UserFollowRepoDB) Unfollow(ctx context.Context, userID string, targetID string) error { 31 + func (repo *UserFollowRepoDB) Unfollow(ctx context.Context, userID int, targetID int) error { 33 32 const statement = /*sql*/ ` 34 33 DELETE FROM user_follows 35 34 WHERE user_id = ? AND target_id = ? ··· 42 41 return nil 43 42 } 44 43 45 - func (repo *UserFollowRepoDB) list(ctx context.Context, userID string, filter pagination.Filter, whereColumn string) ([]models.User, pagination.Page, error) { 46 - var statement = /*sql*/ ` 44 + func (repo *UserFollowRepoDB) ListFollowers(ctx context.Context, userID int, filter pagination.Filter) ([]models.User, pagination.Page, error) { 45 + const statement = /*sql*/ ` 47 46 WITH followers AS ( 48 47 SELECT 49 48 users.id, users.name 50 49 FROM users 51 50 INNER JOIN user_follows ON users.id = user_follows.user_id 52 - WHERE %s = ? 51 + WHERE user_follows.target_id = ? 53 52 ) 54 53 SELECT 55 54 followers.id, followers.name, COUNT(*) OVER () AS total ··· 59 58 ` 60 59 61 60 args := []any{userID, filter.Limit(), filter.Offset()} 62 - statement = fmt.Sprintf(statement, whereColumn) 63 61 64 62 var total int 65 63 users, err := database.QueryMany(ctx, repo.db, statement, args, func(r *sql.Rows) (models.User, error) { ··· 76 74 return users, pagination.New(filter, total), nil 77 75 } 78 76 79 - func (repo *UserFollowRepoDB) ListFollowers(ctx context.Context, userID string, filter pagination.Filter) ([]models.User, pagination.Page, error) { 80 - return repo.list(ctx, userID, filter, "user_follows.target_id") 81 - } 77 + func (repo *UserFollowRepoDB) ListFollowing(ctx context.Context, userID int, filter pagination.Filter) ([]models.User, pagination.Page, error) { 78 + const statement = /*sql*/ ` 79 + WITH following AS ( 80 + SELECT 81 + users.id, users.name 82 + FROM users 83 + INNER JOIN user_follows ON users.id = user_follows.target_id 84 + WHERE user_follows.user_id = ? 85 + ) 86 + SELECT 87 + following.id, following.name, COUNT(*) OVER () AS total 88 + FROM following 89 + ORDER BY following.name 90 + LIMIT ? OFFSET ? 91 + ` 92 + 93 + args := []any{userID, filter.Limit(), filter.Offset()} 94 + 95 + var total int 96 + users, err := database.QueryMany(ctx, repo.db, statement, args, func(r *sql.Rows) (models.User, error) { 97 + var user models.User 98 + if err := r.Scan(&user.ID, &user.Name, &total); err != nil { 99 + return models.User{}, err 100 + } 101 + return user, nil 102 + }) 103 + if err != nil { 104 + return nil, pagination.Page{}, err 105 + } 82 106 83 - func (repo *UserFollowRepoDB) ListFollowing(ctx context.Context, userID string, filter pagination.Filter) ([]models.User, pagination.Page, error) { 84 - return repo.list(ctx, userID, filter, "user_follows.user_id") 107 + return users, pagination.New(filter, total), nil 85 108 }
+4 -4
internal/repo/repo.go
··· 46 46 } 47 47 48 48 type UserFollowRepo interface { 49 - Follow(ctx context.Context, userID string, targetID string) error 50 - Unfollow(ctx context.Context, userID string, targetID string) error 51 - ListFollowers(ctx context.Context, userID string, filter pagination.Filter) ([]models.User, pagination.Page, error) 52 - ListFollowing(ctx context.Context, userID string, filter pagination.Filter) ([]models.User, pagination.Page, error) 49 + Follow(ctx context.Context, userID int, targetID int) error 50 + Unfollow(ctx context.Context, userID int, targetID int) error 51 + ListFollowers(ctx context.Context, userID int, filter pagination.Filter) ([]models.User, pagination.Page, error) 52 + ListFollowing(ctx context.Context, userID int, filter pagination.Filter) ([]models.User, pagination.Page, error) 53 53 }
+12
internal/services/user.go
··· 73 73 74 74 return items, page, nil 75 75 } 76 + 77 + func (us *UserService) ListFollowers( 78 + ctx context.Context, userID int, filter pagination.Filter, 79 + ) ([]models.User, pagination.Page, error) { 80 + return us.userFollowRepo.ListFollowers(ctx, userID, filter) 81 + } 82 + 83 + func (us *UserService) ListFollowing( 84 + ctx context.Context, userID int, filter pagination.Filter, 85 + ) ([]models.User, pagination.Page, error) { 86 + return us.userFollowRepo.ListFollowing(ctx, userID, filter) 87 + }
+14
internal/web/responses/catalog.go
··· 60 60 Items: items, 61 61 } 62 62 } 63 + 64 + type User struct { 65 + ID int `json:"id"` 66 + Name string `json:"name"` 67 + Email string `json:"email"` 68 + } 69 + 70 + func NewUserFromModel(model models.User) User { 71 + return User{ 72 + ID: model.ID, 73 + Name: model.Name, 74 + Email: model.Email, 75 + } 76 + }
+17 -1
internal/web/router/router.go
··· 15 15 "github.com/oscar345/keeptrack/internal/web/responses" 16 16 "github.com/oscar345/keeptrack/pkg/enum" 17 17 "github.com/oscar345/keeptrack/pkg/inertia" 18 + "github.com/oscar345/keeptrack/pkg/pagination" 18 19 "github.com/oscar345/keeptrack/private" 19 20 ) 20 21 ··· 93 94 }) 94 95 95 96 r.Get("/friends", func(w http.ResponseWriter, r *http.Request) { 96 - s.inertia.Render(w, r, "friends/Index", inertia.Props{}) 97 + s.inertia.Render(w, r, "friends/Index", inertia.Props{ 98 + "following": inertia.Lazy(func() (any, error) { 99 + following, page, err := s.userService.ListFollowing(r.Context(), 1, pagination.Filter{}) 100 + if err != nil { 101 + return nil, err 102 + } 103 + return responses.Paginate(page, enum.Map(following, responses.NewUserFromModel)), nil 104 + }), 105 + "followers": inertia.Lazy(func() (any, error) { 106 + followers, page, err := s.userService.ListFollowers(r.Context(), 1, pagination.Filter{}) 107 + if err != nil { 108 + return nil, err 109 + } 110 + return responses.Paginate(page, enum.Map(followers, responses.NewUserFromModel)), nil 111 + }), 112 + }) 97 113 }) 98 114 99 115 r.Get("/mixtapes", func(w http.ResponseWriter, r *http.Request) {
+35
web/components/social/UserListItem.svelte
··· 1 + <script lang="ts"> 2 + import type { User } from "$schemas/responses"; 3 + 4 + type Props = { 5 + user: User; 6 + leading: "image" | "none"; 7 + }; 8 + 9 + let { user, leading = "image" }: Props = $props(); 10 + </script> 11 + 12 + <article> 13 + {#if leading === "image"} 14 + <div class="leading"></div> 15 + {/if} 16 + <div class="text"> 17 + <p>{user.name}</p> 18 + </div> 19 + </article> 20 + 21 + <style> 22 + article { 23 + display: grid; 24 + grid-template-columns: var(--spacing-10) 1fr; 25 + } 26 + 27 + .text { 28 + grid-column-start: 1; 29 + grid-column-end: 3; 30 + } 31 + 32 + article:has(> .leading) > .text { 33 + grid-column-start: 2; 34 + } 35 + </style>
+6
web/lib/.gen/schemas/responses.ts
··· 22 22 total: number; 23 23 items: T[]; 24 24 } 25 + 26 + export type User = { 27 + id: number; 28 + name: string; 29 + email: string; 30 + }
+47 -2
web/views/friends/Index.svelte
··· 1 1 <script lang="ts"> 2 - type Props = {}; 2 + import UserListItem from "$components/social/UserListItem.svelte"; 3 + import type { Page, User } from "$schemas/responses"; 3 4 4 - let {}: Props = $props(); 5 + type Props = { 6 + following: Page<User>; 7 + followers: Page<User>; 8 + }; 9 + 10 + let { following, followers }: Props = $props(); 5 11 </script> 6 12 7 13 <div class="content"> ··· 14 20 </p> 15 21 </hgroup> 16 22 </header> 23 + 24 + <section class="users"> 25 + <header class="header"> 26 + <hgroup> 27 + <h2 class="h2">Following</h2> 28 + </hgroup> 29 + </header> 30 + 31 + <div> 32 + {#each following.items as item} 33 + <UserListItem user={item} leading="none" /> 34 + {/each} 35 + </div> 36 + </section> 37 + 38 + <section class="users"> 39 + <header class="header"> 40 + <hgroup> 41 + <h2 class="h2">Followers</h2> 42 + </hgroup> 43 + </header> 44 + 45 + <div> 46 + {#each followers.items as item} 47 + <UserListItem user={item} leading="none" /> 48 + {/each} 49 + </div> 50 + </section> 17 51 </div> 18 52 19 53 <style> 54 + .users { 55 + display: flex; 56 + flex-direction: column; 57 + gap: var(--spacing-4); 58 + } 59 + 60 + .users > div { 61 + display: flex; 62 + flex-direction: column; 63 + gap: var(--spacing-2); 64 + } 20 65 </style>