diff --git a/README.md b/README.md index b06a446..92f8ba3 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/config.default.yml b/config.default.yml index bbe4228..2a45485 100644 --- a/config.default.yml +++ b/config.default.yml @@ -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 diff --git a/config/config.go b/config/config.go index 480b373..3e7e63b 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/models/summary.go b/models/summary.go index 8db9f68..0407f0e 100644 --- a/models/summary.go +++ b/models/summary.go @@ -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 diff --git a/models/user.go b/models/user.go index faf89bb..8258473 100644 --- a/models/user.go +++ b/models/user.go @@ -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 diff --git a/routes/routes.go b/routes/routes.go index cd3d3c6..f170a58 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -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 + }, } } diff --git a/scripts/bundle_icons.js b/scripts/bundle_icons.js index 08b6da4..369e2f7 100755 --- a/scripts/bundle_icons.js +++ b/scripts/bundle_icons.js @@ -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')) diff --git a/static/assets/app.js b/static/assets/app.js index 4f17456..6810257 100644 --- a/static/assets/app.js +++ b/static/assets/app.js @@ -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') diff --git a/static/assets/icons.js b/static/assets/icons.js index e76ed33..0809245 100644 --- a/static/assets/icons.js +++ b/static/assets/icons.js @@ -1,4 +1,4 @@ -Iconify.addCollection({"prefix":"fxemoji","icons":{"key":{"body":""},"rocket":{"body":""},"satelliteantenna":{"body":""},"lockandkey":{"body":""},"clipboard":{"body":""}},"width":512,"height":512}); +Iconify.addCollection({"prefix":"fxemoji","icons":{"key":{"body":""},"rocket":{"body":""},"satelliteantenna":{"body":""},"lockandkey":{"body":""},"clipboard":{"body":""},"running":{"body":""}},"width":512,"height":512}); Iconify.addCollection({"prefix":"flat-color-icons","icons":{"donate":{"body":""},"clock":{"body":""}},"width":48,"height":48}); Iconify.addCollection({"prefix":"codicon","icons":{"github-inverted":{"body":""}},"width":16,"height":16}); Iconify.addCollection({"prefix":"ant-design","icons":{"check-square-filled":{"body":""}},"width":1024,"height":1024}); @@ -7,3 +7,4 @@ Iconify.addCollection({"prefix":"twemoji","icons":{"light-bulb":{"body":""},"stop-button":{"body":""}},"width":128,"height":128}); Iconify.addCollection({"prefix":"eva","icons":{"corner-right-down-fill":{"body":""}},"width":24,"height":24}); Iconify.addCollection({"prefix":"bi","icons":{"heart-fill":{"body":""}},"width":16,"height":16}); +Iconify.addCollection({"prefix":"ic","icons":{"round-person":{"body":""}},"width":24,"height":24}); diff --git a/static/assets/vendor/tailwind.dist.css b/static/assets/vendor/tailwind.dist.css index 362c85e..7633420 100644 --- a/static/assets/vendor/tailwind.dist.css +++ b/static/assets/vendor/tailwind.dist.css @@ -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; } diff --git a/views/summary.tpl.html b/views/summary.tpl.html index fddb064..aa62311 100644 --- a/views/summary.tpl.html +++ b/views/summary.tpl.html @@ -19,6 +19,24 @@ + +
-
-
- -
+
+ {{ if avatarUrlTemplate }} + User Profile Avatar + {{ else }} + + {{ end }}