feat: user avatars

This commit is contained in:
Ferdinand Mütsch 2021-10-14 12:01:06 +02:00
parent 76a7cf7e80
commit 5df0f48303
11 changed files with 140 additions and 43 deletions

View File

@ -145,6 +145,7 @@ You can specify configuration options either via a config file (default: `config
|---------------------------|---------------------------|--------------|---------------------------------------------------------------------|
| `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.custom_languages` | - | - | Map from file endings to language names |
| `app.avatar_url_template` | - | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |

View File

@ -21,6 +21,10 @@ app:
jsx: JSX
svelte: Svelte
# url template for user avatar images (to be used with services like gravatar or dicebear)
# available variable placeholders are: username, username_hash, email, email_hash
avatar_url_template: https://avatars.dicebear.com/api/pixel-art-neutral/{username_hash}.svg
db:
host: # leave blank when using sqlite3
port: # leave blank when using sqlite3

View File

@ -62,14 +62,15 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
var env string
type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
AvatarURLTemplate string `yaml:"avatar_url_template" default:"https://avatars.dicebear.com/api/pixel-art-neutral/{username_hash}.svg"`
CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
}
type securityConfig struct {

View File

@ -53,6 +53,7 @@ type SummaryViewModel struct {
*Summary
*SummaryParams
User *User
AvatarURL string
LanguageColors map[string]string
EditorColors map[string]string
OSColors map[string]string

View File

@ -1,7 +1,10 @@
package models
import (
"crypto/md5"
"fmt"
"regexp"
"strings"
"time"
)
@ -92,6 +95,18 @@ func (u *User) TZOffset() time.Duration {
return time.Duration(offset * int(time.Second))
}
func (u *User) AvatarURL(urlTemplate string) string {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username}", u.ID)
urlTemplate = strings.ReplaceAll(urlTemplate, "{email}", u.Email)
if strings.Contains(urlTemplate, "{username_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.ID))))
}
if strings.Contains(urlTemplate, "{email_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{email_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.Email))))
}
return urlTemplate
}
func (c *CredentialsReset) IsValid() bool {
return ValidatePassword(c.PasswordNew) &&
c.PasswordNew == c.PasswordRepeat

View File

@ -52,6 +52,9 @@ func DefaultTemplateFuncs() template.FuncMap {
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
"avatarUrlTemplate": func() string {
return config.Get().App.AvatarURLTemplate
},
}
}

View File

@ -32,6 +32,8 @@ let icons = [
'twemoji:gear',
'eva:corner-right-down-fill',
'bi:heart-fill',
'fxemoji:running',
'ic:round-person'
]
const output = path.normalize(path.join(__dirname, '../static/assets/icons.js'))

View File

@ -420,6 +420,13 @@ function hexToRgb(hex) {
} : null;
}
function showUserMenuPopup(event) {
const el = document.getElementById('user-menu-popup')
el.classList.remove('hidden')
el.classList.add('block')
event.stopPropagation()
}
function showApiKeyPopup(event) {
const el = document.getElementById('api-key-popup')
el.classList.remove('hidden')

File diff suppressed because one or more lines are too long

View File

@ -9,8 +9,10 @@
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
@ -51,9 +53,12 @@ h1 {
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
@ -62,8 +67,10 @@ hr {
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
@ -83,10 +90,13 @@ a {
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted; /* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
@ -106,8 +116,10 @@ strong {
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
@ -163,10 +175,14 @@ input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
@ -175,7 +191,8 @@ textarea {
*/
button,
input { /* 1 */
input {
/* 1 */
overflow: visible;
}
@ -185,7 +202,8 @@ input { /* 1 */
*/
button,
select { /* 1 */
select {
/* 1 */
text-transform: none;
}
@ -239,12 +257,18 @@ fieldset {
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
@ -394,8 +418,10 @@ ul {
*/
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */
line-height: 1.5; /* 2 */
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 1 */
line-height: 1.5;
/* 2 */
}
/**
@ -427,10 +453,14 @@ html {
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: #e2e8f0; /* 2 */
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e2e8f0;
/* 2 */
}
/*
@ -723,6 +753,10 @@ video {
border-radius: 9999px;
}
.border-2 {
border-width: 2px;
}
.border {
border-width: 1px;
}
@ -831,6 +865,10 @@ video {
font-weight: 400;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
@ -999,6 +1037,10 @@ video {
margin-top: 4rem;
}
.mt-24 {
margin-top: 6rem;
}
.-ml-1 {
margin-left: -0.25rem;
}

View File

@ -19,6 +19,24 @@
</div>
</div>
<div class="hidden flex bg-gray-800 shadow-md z-10 p-2 absolute top-0 right-0 mt-10 mr-8 border border-green-700 rounded popup mt-24"
id="user-menu-popup" style="min-width: 200px;">
<div class="flex-grow flex flex-col px-2">
<div class="flex flex-col text-xs text-gray-300 mx-1 mb-4 items-center">
<span class="font-semibold">{{ .User.ID }}</span>
{{ if .User.Email }}
<span>({{ .User.Email }})</span>
{{ end }}
</div>
<form action="logout" method="post" class="flex-grow">
<button type="submit" class="py-1 px-3 h-8 rounded bg-green-700 text-white text-sm w-full">
<span>Logout</span>
<span class="iconify inline" data-icon="fxemoji:running"></span>
</button>
</form>
</div>
</div>
<div class="absolute flex top-0 right-0 mr-8 mt-10 py-2">
<div class="mx-1">
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm"
@ -30,10 +48,12 @@
<span class="iconify inline" data-icon="twemoji:gear"></span>
</a>
</div>
<div class="mx-1">
<form action="logout" method="post">
<button type="submit" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm">Logout</button>
</form>
<div class="mx-1 flex items-center">
{{ if avatarUrlTemplate }}
<img src="{{ .User.AvatarURL avatarUrlTemplate }}" width="32px" class="rounded-full border-2 border-green-700 cursor-pointer" onclick="showUserMenuPopup(event)" alt="User Profile Avatar" title="Looks like you, doesn't it?"/>
{{ else }}
<span class="iconify inline cursor-pointer text-gray-500 rounded-full border-2 border-green-700" style="width: 32px; height: 32px" data-icon="ic:round-person" onclick="showUserMenuPopup(event)"></span>
{{ end }}
</div>
</div>