Skip to content

Commit

Permalink
single day/week/month badges (#35)
Browse files Browse the repository at this point in the history
Co-authored-by: Yaroslav Podorvanov <[email protected]>
  • Loading branch information
andrew-pavlov-ua and YaroslavPodorvanov authored Jul 12, 2024
1 parent 8dfb1f9 commit 5980d92
Show file tree
Hide file tree
Showing 17 changed files with 379 additions and 42 deletions.
44 changes: 24 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# [u8views.com](https://u8views.com)
[![Yaroslav Podorvanov profile views](https://u8views.com/api/v1/github/profiles/63663261/views/day-week-month-total-count.svg)](https://u8views.com/github/YaroslavPodorvanov)
[![u8views profile views](https://u8views.com/api/v1/github/profiles/121827373/views/day-week-month-total-count.svg)](https://u8views.com/github/u8views)

### Profile views counter
[![Yaroslav Podorvanov profile views](https://github.com/u8views/go-u8views/blob/main/public/assets/images/yaroslav-podorvanov-developer.jpg?raw=true)](https://u8views.com/github/YaroslavPodorvanov)
Expand Down Expand Up @@ -117,26 +117,30 @@ FROM (
```sql
SELECT DATE_TRUNC('MONTH', time) AS month,
COUNT(*) AS views,
COUNT(DISTINCT (user_id)) AS users
COUNT(DISTINCT (user_id)) AS users,
SUM("count") AS total
FROM profile_hourly_views_stats
GROUP BY 1
ORDER BY 1;

```
| Month | Views | Users |
|------------|-------|-------|
| 2023-01-01 | 15 | 3 |
| 2023-02-01 | 438 | 18 |
| 2023-03-01 | 951 | 32 |
| 2023-04-01 | 1110 | 36 |
| 2023-05-01 | 2191 | 43 |
| 2023-06-01 | 3433 | 57 |
| 2023-07-01 | 3331 | 54 |
| 2023-08-01 | 4539 | 69 |
| 2023-09-01 | 4519 | 77 |
| 2023-10-01 | 4473 | 78 |
| 2023-11-01 | 4919 | 96 |
| 2023-12-01 | 5525 | 115 |
| 2024-01-01 | 11185 | 232 |
| 2024-02-01 | 11348 | 245 |
| 2024-03-01 | 7334 | 249 |
| Month | Views | Users | Total |
|------------|-------|-------|-------|
| 2023-01-01 | 15 | 3 | 78 |
| 2023-02-01 | 438 | 18 | 2700 |
| 2023-03-01 | 951 | 32 | 10241 |
| 2023-04-01 | 1110 | 36 | 3441 |
| 2023-05-01 | 2191 | 43 | 9032 |
| 2023-06-01 | 3433 | 57 | 16866 |
| 2023-07-01 | 3331 | 54 | 14233 |
| 2023-08-01 | 4539 | 69 | 18017 |
| 2023-09-01 | 4519 | 77 | 17053 |
| 2023-10-01 | 4473 | 78 | 15771 |
| 2023-11-01 | 4919 | 96 | 17567 |
| 2023-12-01 | 5525 | 115 | 19882 |
| 2024-01-01 | 11185 | 232 | 40202 |
| 2024-02-01 | 11348 | 245 | 39586 |
| 2024-03-01 | 13581 | 273 | 48629 |
| 2024-04-01 | 13613 | 291 | 47198 |
| 2024-05-01 | 14655 | 306 | 50581 |
| 2024-06-01 | 13924 | 319 | 57695 |
| 2024-07-01 | 5692 | 293 | 25950 |
5 changes: 5 additions & 0 deletions cmd/v3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ func main() {
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/stats.json", statsController.GitHubStats)
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/day-week-month-total-count.svg", statsController.GitHubDayWeekMonthTotalCountBadge)
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/pixel.svg", statsController.Pixel)

r.GET("/api/v1/github/profiles/:social_provider_user_id/views/day-count.svg", statsController.DayCountBadge)
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/week-count.svg", statsController.WeekCountBadge)
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/month-count.svg", statsController.MonthCountBadge)
r.GET("/api/v1/github/profiles/:social_provider_user_id/views/total-count.svg", statsController.TotalCountBadge)

r.GET("/api/v1/github/profiles/:social_provider_user_id/referrals/stats.json", statsController.ReferralsStats)
r.GET("/api/v1/users/stats.json", statsController.UsersCreatedAtStatsByDay)

Expand Down
90 changes: 74 additions & 16 deletions internal/controllers/stats_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"strings"
"time"

"github.com/u8views/go-u8views/internal/badge"
"github.com/u8views/go-u8views/internal/services"
Expand Down Expand Up @@ -53,6 +54,39 @@ func (c *StatsController) GitHubDayWeekMonthTotalCount(ctx *gin.Context) {
})
}

func (c *StatsController) DayCountBadge(ctx *gin.Context) {
period := time.Now().UTC().Truncate(time.Hour).AddDate(0, 0, -1)

statsCount, done := c.timePeriodStatsCount(ctx, dbs.SocialProviderGithub, period)
if done {
return
}

c.renderImage(ctx, []byte(tmv2.DayBadge(statsCount)))
}

func (c *StatsController) WeekCountBadge(ctx *gin.Context) {
period := time.Now().UTC().Truncate(time.Hour).AddDate(0, 0, -7)

statsCount, done := c.timePeriodStatsCount(ctx, dbs.SocialProviderGithub, period)
if done {
return
}

c.renderImage(ctx, []byte(tmv2.WeekBadge(statsCount)))
}

func (c *StatsController) MonthCountBadge(ctx *gin.Context) {
period := time.Now().UTC().Truncate(time.Hour).AddDate(0, -1, 0)

statsCount, done := c.timePeriodStatsCount(ctx, dbs.SocialProviderGithub, period)
if done {
return
}

c.renderImage(ctx, []byte(tmv2.MonthBadge(statsCount)))
}

func (c *StatsController) TotalCountBadge(ctx *gin.Context) {
statsCount, done := c.statsCount(ctx, dbs.SocialProviderGithub)
if done {
Expand All @@ -70,10 +104,7 @@ func (c *StatsController) TotalCountBadge(ctx *gin.Context) {
return
}

ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Pragma", "no-cache")
ctx.Header("Expires", "0")
ctx.Data(http.StatusOK, "image/svg+xml", []byte(totalCountBadge))
c.renderImage(ctx, totalCountBadge)
}

func (c *StatsController) GitHubDayWeekMonthTotalCountBadge(ctx *gin.Context) {
Expand All @@ -82,10 +113,7 @@ func (c *StatsController) GitHubDayWeekMonthTotalCountBadge(ctx *gin.Context) {
return
}

ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Pragma", "no-cache")
ctx.Header("Expires", "0")
ctx.Data(http.StatusOK, "image/svg+xml", []byte(tmv2.Badge(statsCount)))
c.renderImage(ctx, []byte(tmv2.Badge(statsCount)))
}

func (c *StatsController) Pixel(ctx *gin.Context) {
Expand All @@ -101,10 +129,7 @@ func (c *StatsController) Pixel(ctx *gin.Context) {
return
}

ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Pragma", "no-cache")
ctx.Header("Expires", "0")
ctx.Data(http.StatusOK, "image/svg+xml", []byte(pixel))
c.renderImage(ctx, []byte(pixel))
}

func (c *StatsController) GitHubStats(ctx *gin.Context) {
Expand Down Expand Up @@ -172,6 +197,33 @@ func (c *StatsController) ReferralsStats(ctx *gin.Context) {
ctx.JSON(http.StatusOK, result)
}

func (c *StatsController) timePeriodStatsCount(ctx *gin.Context, provider dbs.SocialProvider, period time.Time) (statsCount int64, done bool) {
socialProviderUserID, done := c.parseSocialProviderUserID(ctx)
if done {
return
}

userID, done := c.toUserID(ctx, provider, socialProviderUserID)
if done {
return
}

increment := strings.HasPrefix(ctx.GetHeader("User-Agent"), "github-camo")

statsCount, err := c.statsService.TimePeriodStatsCount(ctx, userID, increment, period)
if err != nil {
log.Printf("Database error (stats) %s\n", err)

ctx.JSON(http.StatusInternalServerError, &ErrorResponse{
ErrorMessage: "Database error",
})

return statsCount, true
}

return statsCount, false
}

func (c *StatsController) statsCount(ctx *gin.Context, provider dbs.SocialProvider) (statsCount services.ProfileViewsStats, done bool) {
socialProviderUserID, done := c.parseSocialProviderUserID(ctx)
if done {
Expand Down Expand Up @@ -265,7 +317,6 @@ func (c *StatsController) toUserID(ctx *gin.Context, provider dbs.SocialProvider
}
if err != nil {
log.Printf("Database error (social) %s\n", err)

ctx.JSON(http.StatusInternalServerError, &ErrorResponse{
ErrorMessage: "Database error",
})
Expand All @@ -276,14 +327,21 @@ func (c *StatsController) toUserID(ctx *gin.Context, provider dbs.SocialProvider
return userID, false
}

func createBadge(subject string, count int64) (string, error) {
func (c *StatsController) renderImage(ctx *gin.Context, data []byte) {
ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Pragma", "no-cache")
ctx.Header("Expires", "0")
ctx.Data(http.StatusOK, "image/svg+xml", data)
}

func createBadge(subject string, count int64) ([]byte, error) {
svg, err := badge.RenderBytes(
subject,
strconv.FormatInt(count, 10),
)
if err != nil {
return "", err
return nil, err
}

return string(svg), nil
return svg, nil
}
25 changes: 25 additions & 0 deletions internal/services/stats_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ func (s *StatsService) TotalCount(ctx context.Context, userID int64) (int64, err
return s.repository.Queries().ProfileTotalViews(ctx, userID)
}

func (s *StatsService) TimePeriodStatsCount(ctx context.Context, userID int64, increment bool, timeAfter time.Time) (int64, error) {
statsCount, err := s.repository.Queries().ProfileHourlyViewsStatsByPeriod(ctx, dbs.ProfileHourlyViewsStatsByPeriodParams{
UserID: userID,
TimeAfter: timeAfter,
})
if err != nil {
log.Printf("Database err %s\n", err)

return statsCount, err
}

if increment {
statsCount += 1

go func() {
err := s.increment(context.Background(), userID, time.Now().UTC().Truncate(time.Hour))
if err != nil {
log.Printf("Database err %s\n", err)
}
}()
}

return statsCount, nil
}

func (s *StatsService) StatsCount(ctx context.Context, userID int64, increment bool) (result ProfileViewsStats, err error) {
totalCount, err := s.repository.Queries().ProfileTotalViews(ctx, userID)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions internal/storage/dbs/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion internal/storage/dbs/profile_hourly_views_stats.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/storage/dbs/referrals.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/storage/dbs/users.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion internal/storage/queries/profile_hourly_views_stats.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SELECT g.time AS time,
COALESCE(phvs.count, 0)::BIGINT AS count
FROM (
SELECT time::TIMESTAMP
FROM generate_series(
FROM GENERATE_SERIES(
sqlc.arg('from')::TIMESTAMP,
sqlc.arg('to')::TIMESTAMP,
'1 HOUR'::INTERVAL
Expand All @@ -33,3 +33,9 @@ FROM (
AND time >= sqlc.arg('from')::TIMESTAMP
) AS phvs ON (g.time = phvs.time)
ORDER BY g.time;

-- name: ProfileHourlyViewsStatsByPeriod :one
SELECT COALESCE(SUM(count), 0)::BIGINT AS count
FROM profile_hourly_views_stats
WHERE user_id = @user_id
AND time >= @time_after;
2 changes: 1 addition & 1 deletion internal/storage/queries/referrals.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SELECT g.time AS time,
COALESCE(rc.count, 0)::BIGINT AS count
FROM (
SELECT time::TIMESTAMP
FROM generate_series(
FROM GENERATE_SERIES(
sqlc.arg('from')::DATE,
sqlc.arg('to')::DATE,
'1 DAY'::INTERVAL
Expand Down
2 changes: 1 addition & 1 deletion internal/storage/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ SELECT g.time AS time,
COALESCE(rcs.count, 0)::BIGINT AS count
FROM (
SELECT time::TIMESTAMP
FROM generate_series(
FROM GENERATE_SERIES(
sqlc.arg('from')::DATE,
sqlc.arg('to')::DATE,
'1 DAY'::INTERVAL
Expand Down
18 changes: 18 additions & 0 deletions internal/templates/v2/day_badge.qtpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% func DayBadge(stats int64) %}
<svg width="568" height="24" viewBox="0 0 568 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="113" height="24" rx="4" fill="#24292F"></rect>
<rect x="112" width="54" height="24" rx="4" fill="#6D96FF"></rect>
<rect x="112" width="1" height="24" fill="#E4EAF1"></rect>
<g
fill="#fff"
text-anchor="middle"
font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
font-size="14"
>
<text x="57.5" y="16">Views per day</text>
<text x="138" y="16">{%dl stats %}</text>
</g>
</g>
</svg>
{% endfunc %}
Loading

0 comments on commit 5980d92

Please sign in to comment.