mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Resolve #11.
This commit is contained in:
parent
b7f700e7a5
commit
6d3891b398
@ -1,3 +1,4 @@
|
||||
ENV=prod
|
||||
WAKAPI_DB_USER=myuser
|
||||
WAKAPI_DB_PASSWORD=mysecretpassword
|
||||
WAKAPI_DB_HOST=localhost
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 📈 wakapi
|
||||
**A minimalist, self-hosted WakaTime-compatible backend for coding statistics**
|
||||
|
||||
![Wakapi screenshot](https://anchr.io/i/zCVbN.png)
|
||||
![Wakapi screenshot](https://anchr.io/i/bxQ69.png)
|
||||
|
||||
[![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoff.ee/n1try)
|
||||
|
||||
@ -61,9 +61,6 @@ However, if you want to expose your wakapi instance to the public anyway, you ne
|
||||
## Todo
|
||||
* User sign up and log in
|
||||
* Additional endpoints for retrieving statistics data
|
||||
* Enhanced UI
|
||||
* Loading spinner
|
||||
* Responsiveness
|
||||
* Support for SQLite database
|
||||
* Unit tests
|
||||
|
||||
|
2
main.go
2
main.go
@ -31,6 +31,7 @@ func readConfig() *models.Config {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
env, _ := os.LookupEnv("ENV")
|
||||
dbUser, valid := os.LookupEnv("WAKAPI_DB_USER")
|
||||
dbPassword, valid := os.LookupEnv("WAKAPI_DB_PASSWORD")
|
||||
dbHost, valid := os.LookupEnv("WAKAPI_DB_HOST")
|
||||
@ -62,6 +63,7 @@ func readConfig() *models.Config {
|
||||
}
|
||||
|
||||
return &models.Config{
|
||||
Env: env,
|
||||
Port: port,
|
||||
Addr: addr,
|
||||
DbHost: dbHost,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
type Config struct {
|
||||
Env string
|
||||
Port int
|
||||
Addr string
|
||||
DbHost string
|
||||
@ -12,3 +13,7 @@ type Config struct {
|
||||
DbMaxConn uint
|
||||
CustomLanguages map[string]string
|
||||
}
|
||||
|
||||
func (c *Config) IsDev() bool {
|
||||
return c.Env == "dev"
|
||||
}
|
||||
|
@ -28,18 +28,20 @@ type SummaryHandler struct {
|
||||
}
|
||||
|
||||
func (m *SummaryHandler) Init() {
|
||||
m.loadTemplates()
|
||||
m.Initialized = true
|
||||
}
|
||||
|
||||
func (m *SummaryHandler) loadTemplates() {
|
||||
indexTplPath := "views/index.tpl.html"
|
||||
indexTpl, err := template.New(path.Base(indexTplPath)).Funcs(template.FuncMap{
|
||||
"json": utils.Json,
|
||||
"date": utils.FormatDateHuman,
|
||||
}).ParseFiles(indexTplPath)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.indexTemplate = indexTpl
|
||||
|
||||
m.Initialized = true
|
||||
}
|
||||
|
||||
func (h *SummaryHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
@ -72,6 +74,10 @@ func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) {
|
||||
h.Init()
|
||||
}
|
||||
|
||||
if h.SummarySrvc.Config.IsDev() {
|
||||
h.loadTemplates()
|
||||
}
|
||||
|
||||
summary, err, status := loadUserSummary(r, h.SummarySrvc)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
|
@ -1,44 +1,3 @@
|
||||
body {
|
||||
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
width: 75%;
|
||||
display: grid;
|
||||
grid-template-areas: 'header header header' 'sec1 sec1 sec2' 'sec1 sec1 sec3' 'sec1 sec1 sec4' 'footer footer footer';
|
||||
grid-gap: 10px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.projects-container {
|
||||
grid-area: sec1
|
||||
}
|
||||
|
||||
.os-container {
|
||||
grid-area: sec2
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
grid-area: sec3
|
||||
}
|
||||
|
||||
.language-container {
|
||||
grid-area: sec4
|
||||
}
|
||||
|
||||
.header-container {
|
||||
grid-area: header
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 300px;
|
||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
}
|
@ -18,6 +18,10 @@ func FormatDate(date time.Time) string {
|
||||
return date.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func FormatDateHuman(date time.Time) string {
|
||||
return date.Format("Mon, 02 Jan 2006 15:04")
|
||||
}
|
||||
|
||||
func ParseUserAgent(ua string) (string, string, error) {
|
||||
re := regexp.MustCompile(`^wakatime\/[\d+.]+\s\((\w+).*\)\s.+\s(\w+)\/.+$`)
|
||||
groups := re.FindAllStringSubmatch(ua, -1)
|
||||
|
@ -1,54 +1,74 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Coding Stats</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>Wakapi – Coding Statistics</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<link rel="icon" data-emoji="📊" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="/assets/app.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Wakapi</h1>
|
||||
<h3>Your Coding Statistics Dashboard</h3>
|
||||
<div>
|
||||
<a href="/?interval=today">Today (live)</a>
|
||||
<a href="/?interval=day">Yesterday</a>
|
||||
<a href="/?interval=week">This Week</a>
|
||||
<a href="/?interval=month">This Month</a>
|
||||
<a href="/?interval=year">This Year</a>
|
||||
<a href="/?interval=any">All Time</a>
|
||||
</div>
|
||||
<div class="grid-container" id="grid-container">
|
||||
<div class="header-container">
|
||||
<p>
|
||||
<strong>Total:</strong> <span id="total-span"></span><br>
|
||||
</p>
|
||||
<body class="bg-gray-800 text-gray-700 p-4 pt-12 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
|
||||
</div>
|
||||
<div class="projects-container" id="projects-container">
|
||||
<canvas id="chart-projects"></canvas>
|
||||
<div class="text-white text-sm flex items-center justify-center mt-4">
|
||||
<a href="/?interval=today" class="m-1 border-b border-green-700">Today (live)</a>
|
||||
<a href="/?interval=day" class="m-1 border-b border-green-700">Yesterday</a>
|
||||
<a href="/?interval=week" class="m-1 border-b border-green-700">This Week</a>
|
||||
<a href="/?interval=month" class="m-1 border-b border-green-700">This Month</a>
|
||||
<a href="/?interval=year" class="m-1 border-b border-green-700">This Year</a>
|
||||
<a href="/?interval=any" class="m-1 border-b border-green-700">All Time</a>
|
||||
</div>
|
||||
<div class="os-container" id="os-container">
|
||||
<canvas id="chart-os"></canvas>
|
||||
</div>
|
||||
<div class="editor-container" id="editor-container">
|
||||
<canvas id="chart-editor"></canvas>
|
||||
</div>
|
||||
<div class="language-container" id="language-container">
|
||||
<canvas id="chart-language"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<main class="mt-12 flex-grow" id="grid-container">
|
||||
<div class="flex justify-center">
|
||||
<div class="p-1">
|
||||
<div class="flex justify-center p-4 bg-white rounded shadow">
|
||||
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime | date }}</span></p>
|
||||
<p class="mx-2"><strong>⏹</strong> <span title="End Time">{{ .ToTime | date }}</span></p>
|
||||
<p class="mx-2"><strong>⏱</strong> <span id="total-span" title="Total Hours"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="p-4 bg-white rounded shadow m-2" id="projects-container">
|
||||
<canvas id="chart-projects"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="p-4 bg-white rounded shadow m-2" id="os-container">
|
||||
<canvas id="chart-os"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="p-4 bg-white rounded shadow m-2" id="editor-container">
|
||||
<canvas id="chart-editor"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="p-4 bg-white rounded shadow m-2" id="language-container">
|
||||
<canvas id="chart-language"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="w-full text-center text-gray-300 text-xs mt-12">
|
||||
Made by <a href="https://muetsch.io" class="border-b border-green-700">Ferdinand Mütsch</a> as <a href="https://github.com/n1try/wakapi" class="border-b border-green-700">open-source</a>.
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
let wakapiData = {}
|
||||
wakapiData.projects = {{ json .Projects }}
|
||||
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
|
||||
wakapiData.editors = {{ .Editors | json }}
|
||||
wakapiData.languages = {{ .Languages | json }}
|
||||
</script>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script>
|
||||
let wakapiData = {}
|
||||
wakapiData.projects = {{ json .Projects }}
|
||||
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
|
||||
wakapiData.editors = {{ .Editors | json }}
|
||||
wakapiData.languages = {{ .Languages | json }}
|
||||
</script>
|
||||
<script src="/assets/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user