Skip to content

Commit

Permalink
feat: dark mode (#177)
Browse files Browse the repository at this point in the history
* Created base svg renderer

* Improve renderigng

* Added roboto font

* Added ticks calcaulation

* Added chart rendering

* Fixed 2 pixel shifting for y axis

* Moved chart styles in constant

* Replaced go-chart to internal chart

* Added correct dark theme

* Updated .gitignore

* Refactoring

Refactored font embedding

* Fixed corners rendering

* Removed debug code

* Refactoring

* Minimize memory allocaitons

* Moved colours to css variables

* Fixed html

* Color sheme draft

WIP

* Added chart variants

* Added adaptive favicon

* Added query params colors

* Added custom color impoementation

* WIP

* Added configuration to cosmtrek/air

* Added chart caching

* Final updating markup

* Fixed errors rendering

* Cleanup
  • Loading branch information
evg4b committed Jan 2, 2024
1 parent ca886b9 commit dce1e80
Show file tree
Hide file tree
Showing 43 changed files with 1,963 additions and 347 deletions.
10 changes: 10 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = "."

[build]
include_ext = ["go", "css", "svg", "html", "js", "gohtml"]
stop_on_error = false
rerun_delay = 0

[misc]
# Delete tmp directory on exit
clean_on_exit = true
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4

[{*.go,*.go2}]
indent_style = tab

[*.svg]
indent_size = 2
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ vendor
bin
coverage.txt
dist
.idea
.DS_Store
tmp
120 changes: 120 additions & 0 deletions controller/chart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package controller

import (
"fmt"
"github.com/apex/log"
"github.com/caarlos0/httperr"
"github.com/caarlos0/starcharts/internal/cache"
"github.com/caarlos0/starcharts/internal/chart"
"github.com/caarlos0/starcharts/internal/chart/svg"
"github.com/caarlos0/starcharts/internal/github"
"io"
"net/http"
"strings"
"time"
)

var stylesMap = map[string]string{
"light": chart.LightStyles,
"dark": chart.DarkStyles,
"adaptive": chart.AdaptiveStyles,
}

// GetRepoChart returns the SVG chart for the given repository.
//
// nolint: funlen
// TODO: refactor.
func GetRepoChart(gh *github.GitHub, cache *cache.Redis) http.Handler {
return httperr.NewF(func(w http.ResponseWriter, r *http.Request) error {
params, err := extractSvgChartParams(r)
if err != nil {
log.WithError(err).Error("failed to extract params")
return err
}

cacheKey := chartKey(params)
name := fmt.Sprintf("%s/%s", params.Owner, params.Repo)
log := log.WithField("repo", name).WithField("variant", params.Variant)

cachedChart := ""
if err = cache.Get(cacheKey, &cachedChart); err == nil {
writeSvgHeaders(w)
log.Debug("using cached chart")
_, err := fmt.Fprint(w, cachedChart)
return err
}

defer log.Trace("collect_stars").Stop(nil)
repo, err := gh.RepoDetails(r.Context(), name)
if err != nil {
return httperr.Wrap(err, http.StatusBadRequest)
}

stargazers, err := gh.Stargazers(r.Context(), repo)
if err != nil {
log.WithError(err).Error("failed to get stars")
writeSvgHeaders(w)
_, err = w.Write([]byte(errSvg(err)))
return err
}

series := chart.Series{
StrokeWidth: 2,
Color: params.Line,
}
for i, star := range stargazers {
series.XValues = append(series.XValues, star.StarredAt)
series.YValues = append(series.YValues, float64(i))
}
if len(series.XValues) < 2 {
log.Info("not enough results, adding some fake ones")
series.XValues = append(series.XValues, time.Now())
series.YValues = append(series.YValues, 1)
}

graph := &chart.Chart{
Width: CHART_WIDTH,
Height: CHART_HEIGHT,
Styles: stylesMap[params.Variant],
Background: params.Background,
XAxis: chart.XAxis{
Name: "Time",
Color: params.Axis,
StrokeWidth: 2,
},
YAxis: chart.YAxis{
Name: "Stargazers",
Color: params.Axis,
StrokeWidth: 2,
},
Series: series,
}
defer log.Trace("chart").Stop(&err)

writeSvgHeaders(w)

cacheBuffer := &strings.Builder{}
graph.Render(io.MultiWriter(w, cacheBuffer))
err = cache.Put(cacheKey, cacheBuffer.String())
if err != nil {
log.WithError(err).Error("failed to cache chart")
}

return nil
})
}

func errSvg(err error) string {
return svg.SVG().
Attr("width", svg.Px(CHART_WIDTH)).
Attr("height", svg.Px(CHART_HEIGHT)).
ContentFunc(func(writer io.Writer) {
svg.Text().
Attr("fill", "red").
Attr("x", svg.Px(CHART_WIDTH/2)).
Attr("y", svg.Px(CHART_HEIGHT/2)).
Content(err.Error()).
Render(writer)
}).
String()
}
87 changes: 87 additions & 0 deletions controller/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package controller

import (
"fmt"
"github.com/gorilla/mux"
"net/http"
"regexp"
"time"
)

const (
base = "static/templates/base.gohtml"
repository = "static/templates/repository.gohtml"
index = "static/templates/index.gohtml"
)

var colorExpression = regexp.MustCompile("^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3}|[a-fA-F0-9]{8})$")

func extractColor(r *http.Request, name string) (string, error) {
color := r.URL.Query().Get(name)
if len(color) == 0 {
return "", nil
}

if colorExpression.MatchString(color) {
return color, nil
}

return "", fmt.Errorf("invalid %s: %s", name, color)
}

type params struct {
Owner string
Repo string
Line string
Background string
Axis string
Variant string
}

func extractSvgChartParams(r *http.Request) (*params, error) {
backgroundColor, err := extractColor(r, "background")
if err != nil {
return nil, err
}

axisColor, err := extractColor(r, "axis")
if err != nil {
return nil, err
}

lineColor, err := extractColor(r, "line")
if err != nil {
return nil, err
}

vars := mux.Vars(r)

return &params{
Owner: vars["owner"],
Repo: vars["repo"],
Background: backgroundColor,
Axis: axisColor,
Line: lineColor,
Variant: r.URL.Query().Get("variant"),
}, nil
}

func writeSvgHeaders(w http.ResponseWriter) {
header := w.Header()
header.Add("content-type", "image/svg+xml;charset=utf-8")
header.Add("cache-control", "public, max-age=86400")
header.Add("date", time.Now().Format(time.RFC1123))
header.Add("expires", time.Now().Format(time.RFC1123))
}

func chartKey(params *params) string {
return fmt.Sprintf(
"%s/%s/[%s][%s][%s][%s]",
params.Owner,
params.Repo,
params.Variant,
params.Background,
params.Axis,
params.Line,
)
}
17 changes: 8 additions & 9 deletions controller/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@ package controller

import (
"html/template"
"io"
"io/fs"
"net/http"
"strings"

"github.com/caarlos0/httperr"
)

func Index(fsys fs.FS, version string) http.Handler {
func Index(filesystem fs.FS, version string) http.Handler {
indexTemplate, err := template.ParseFS(filesystem, base, index)
if err != nil {
panic(err)
}

return httperr.NewF(func(w http.ResponseWriter, r *http.Request) error {
return executeTemplate(fsys, w, map[string]string{"Version": version})
return indexTemplate.Execute(w, map[string]string{"Version": version})
})
}

func HandleForm(fsys fs.FS) http.HandlerFunc {
func HandleForm() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
repo := strings.TrimPrefix(r.FormValue("repository"), "https://github.com/")
http.Redirect(w, r, repo, http.StatusSeeOther)
}
}

func executeTemplate(fsys fs.FS, w io.Writer, data interface{}) error {
return template.Must(template.ParseFS(fsys, "static/templates/index.html")).
Execute(w, data)
}

0 comments on commit dce1e80

Please sign in to comment.