feat: add stats page

This commit is contained in:
Krispire Admin
2026-05-31 14:54:15 +05:30
parent d3ad443fde
commit 72f4e6527c
14 changed files with 750 additions and 36 deletions
+66 -31
View File
@@ -126,41 +126,68 @@ func (r *PostRepo) FindByID(ctx context.Context, id string) (*model.Post, error)
return p, nil
}
// ListByHandle returns the public feed for a user, newest-first, paginated.
// ListByHandle returns the profile feed for a user — authored posts
// plus posts the user reshared — newest-first, paginated. Resolves
// handle → user_id and then delegates to ListByUser so both code paths
// produce the same shape (and so the union query lives in one place).
// Unknown handles return an empty page (not an error) — that matches
// the pre-reshare behaviour.
func (r *PostRepo) ListByHandle(ctx context.Context, handle string, limit, offset int) ([]model.Post, int, error) {
var total int
if err := r.pg.Pool.QueryRow(ctx, `
SELECT COUNT(*) FROM posts p
JOIN users u ON u.id = p.user_id
WHERE u.handle = $1`, handle,
).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("count posts: %w", err)
var userID string
if err := r.pg.Pool.QueryRow(ctx, `SELECT id FROM users WHERE handle = $1`, handle).Scan(&userID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return []model.Post{}, 0, nil
}
return nil, 0, fmt.Errorf("lookup user by handle: %w", err)
}
const q = `
SELECT p.id, p.user_id, p.caption, p.source_url, COALESCE(p.source_title, ''),
p.created_at, p.updated_at, u.name, u.handle, u.avatar_url
FROM posts p
JOIN users u ON u.id = p.user_id
WHERE u.handle = $1
ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3`
return r.collectPosts(ctx, total, q, handle, limit, offset)
return r.ListByUser(ctx, userID, limit, offset)
}
// ListByUser returns the user's own posts (private listing — same shape).
// ListByUser returns the user's profile feed — authored posts plus
// posts they reshared. Each row carries a reshared_at timestamp that
// is NULL for authored rows and equal to post_reshares.created_at for
// reshared rows; rows are sorted by COALESCE(reshared_at, created_at)
// so fresh reshares of old posts surface at the top, matching the
// Twitter/X model.
//
// `total` is the union count so pagination spans the combined feed.
func (r *PostRepo) ListByUser(ctx context.Context, userID string, limit, offset int) ([]model.Post, int, error) {
var total int
if err := r.pg.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM posts WHERE user_id = $1`, userID).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("count posts: %w", err)
if err := r.pg.Pool.QueryRow(ctx, `
SELECT
(SELECT COUNT(*) FROM posts WHERE user_id = $1)
+ (SELECT COUNT(*) FROM post_reshares WHERE user_id = $1)`,
userID,
).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("count feed: %w", err)
}
const q = `
SELECT p.id, p.user_id, p.caption, p.source_url, COALESCE(p.source_title, ''),
p.created_at, p.updated_at, u.name, u.handle, u.avatar_url
FROM posts p
JOIN users u ON u.id = p.user_id
WHERE p.user_id = $1
ORDER BY p.created_at DESC
SELECT id, user_id, caption, source_url, source_title,
created_at, updated_at, author_name, author_handle,
author_avatar_url, reshared_at
FROM (
SELECT p.id, p.user_id, p.caption, p.source_url,
COALESCE(p.source_title, '') AS source_title,
p.created_at, p.updated_at,
u.name AS author_name, u.handle AS author_handle,
u.avatar_url AS author_avatar_url,
NULL::timestamptz AS reshared_at
FROM posts p
JOIN users u ON u.id = p.user_id
WHERE p.user_id = $1
UNION ALL
SELECT p.id, p.user_id, p.caption, p.source_url,
COALESCE(p.source_title, '') AS source_title,
p.created_at, p.updated_at,
u.name AS author_name, u.handle AS author_handle,
u.avatar_url AS author_avatar_url,
r.created_at AS reshared_at
FROM post_reshares r
JOIN posts p ON p.id = r.post_id
JOIN users u ON u.id = p.user_id
WHERE r.user_id = $1
) feed
ORDER BY COALESCE(reshared_at, created_at) DESC, id
LIMIT $2 OFFSET $3`
return r.collectPosts(ctx, total, q, userID, limit, offset)
}
@@ -178,13 +205,20 @@ func (r *PostRepo) Delete(ctx context.Context, postID, userID string) error {
return nil
}
// CountByUser is used by services that need to know how many posts a user
// has — currently the public-profile endpoint only.
// CountByUser returns the profile-feed total — authored + reshared —
// which is what the profile header's "N posts" badge displays. Stays
// in sync with ListByUser's total so the badge never disagrees with
// the page contents below it.
func (r *PostRepo) CountByUser(ctx context.Context, userID string) (int, error) {
var n int
err := r.pg.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM posts WHERE user_id = $1`, userID).Scan(&n)
err := r.pg.Pool.QueryRow(ctx, `
SELECT
(SELECT COUNT(*) FROM posts WHERE user_id = $1)
+ (SELECT COUNT(*) FROM post_reshares WHERE user_id = $1)`,
userID,
).Scan(&n)
if err != nil {
return 0, fmt.Errorf("count user posts: %w", err)
return 0, fmt.Errorf("count feed: %w", err)
}
return n, nil
}
@@ -206,6 +240,7 @@ func (r *PostRepo) collectPosts(ctx context.Context, total int, query string, ar
&p.ID, &p.UserID, &p.Caption, &p.SourceURL, &p.SourceTitle,
&p.CreatedAt, &p.UpdatedAt,
&p.AuthorName, &p.AuthorHandle, &p.AuthorAvatarURL,
&p.ResharedAt,
); err != nil {
return nil, 0, fmt.Errorf("scan post: %w", err)
}