Look & Feel settings for profile pages

This commit is contained in:
Noah Petherbridge 2023-12-26 15:44:34 -08:00
parent d808741752
commit 90270572f1
9 changed files with 444 additions and 6 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
nm "net/mail" nm "net/mail"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -35,6 +36,7 @@ func (t ChangeEmailToken) Delete() error {
// User settings page. (/settings). // User settings page. (/settings).
func Settings() http.HandlerFunc { func Settings() http.HandlerFunc {
tmpl := templates.Must("account/settings.html") tmpl := templates.Must("account/settings.html")
var reHexColor = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := map[string]interface{}{ vars := map[string]interface{}{
"Enum": config.ProfileEnums, "Enum": config.ProfileEnums,
@ -119,6 +121,62 @@ func Settings() http.HandlerFunc {
} }
session.Flash(w, r, "Profile settings updated!") session.Flash(w, r, "Profile settings updated!")
case "look":
hashtag = "#look"
// Resetting all styles?
if r.PostFormValue("reset") == "true" {
// Blank out all profile fields.
for _, field := range []string{
"hero-color-start",
"hero-color-end",
"hero-text-dark",
"card-title-bg",
"card-title-fg",
"card-link-color",
"card-lightness",
} {
user.SetProfileField(field, "")
}
if err := user.Save(); err != nil {
session.FlashError(w, r, "Failed to save user to database: %s", err)
}
session.Flash(w, r, "Profile look & feel reset to defaults!")
break
}
// Set color preferences.
for _, field := range []string{
"hero-color-start",
"hero-color-end",
"card-title-bg",
"card-title-fg",
"card-link-color",
} {
// Ensure valid.
value := r.PostFormValue(field)
if !reHexColor.Match([]byte(value)) {
value = ""
}
user.SetProfileField(field, value)
}
// Set other fields.
for _, field := range []string{
"hero-text-dark",
"card-lightness",
} {
value := r.PostFormValue(field)
user.SetProfileField(field, value)
}
if err := user.Save(); err != nil {
session.FlashError(w, r, "Failed to save user to database: %s", err)
}
session.Flash(w, r, "Profile look & feel updated!")
case "preferences": case "preferences":
hashtag = "#prefs" hashtag = "#prefs"
var ( var (

View File

@ -134,6 +134,7 @@ var baseTemplates = []string{
config.TemplatePath + "/partials/user_avatar.html", config.TemplatePath + "/partials/user_avatar.html",
config.TemplatePath + "/partials/like_modal.html", config.TemplatePath + "/partials/like_modal.html",
config.TemplatePath + "/partials/right_click.html", config.TemplatePath + "/partials/right_click.html",
config.TemplatePath + "/partials/themes.html",
} }
// templates returns a template chain with the base templates preceding yours. // templates returns a template chain with the base templates preceding yours.

View File

@ -4400,7 +4400,7 @@
.table td, .table td,
.table th { .table th {
border: 1px solid #363636; border-color: #363636;
} }
.table td.is-white, .table td.is-white,

View File

@ -1,5 +1,8 @@
{{define "title"}}Friends of {{.User.Username}}{{end}} {{define "title"}}Friends of {{.User.Username}}{{end}}
{{define "content"}} {{define "content"}}
<style type="text/css">
{{template "profile-theme-hero-style" .User}}
</style>
<div class="container"> <div class="container">
{{$Root := .}} {{$Root := .}}
<section class="hero is-link is-bold"> <section class="hero is-link is-bold">

View File

@ -1,5 +1,6 @@
{{define "title"}}{{.User.Username}}{{end}} {{define "title"}}{{.User.Username}}{{end}}
{{define "content"}} {{define "content"}}
{{template "profile-theme-style" .User}}
<div class="container"> <div class="container">
<section class="hero {{if and .LoggedIn (not .IsPrivate)}}is-info{{else}}is-light is-bold{{end}}"> <section class="hero {{if and .LoggedIn (not .IsPrivate)}}is-info{{else}}is-light is-bold{{end}}">
<div class="hero-body"> <div class="hero-body">
@ -370,7 +371,7 @@
</header> </header>
<div class="card-content"> <div class="card-content">
<table class="table is-fullwidth is-hoverable" style="font-size: small"> <table class="table is-fullwidth" style="font-size: small">
<tr> <tr>
<td class="has-text-right"> <td class="has-text-right">
<strong class="is-size-7">Age:</label> <strong class="is-size-7">Age:</label>

View File

@ -34,6 +34,15 @@
</a> </a>
</li> </li>
<li>
<a href="/settings#look" class="nonshy-tab-button">
<strong><i class="fa fa-palette mr-1"></i> Look &amp; Feel</strong>
<p class="help">
Customize your profile with a color scheme.
</p>
</a>
</li>
<li> <li>
<a href="/settings#prefs" class="nonshy-tab-button"> <a href="/settings#prefs" class="nonshy-tab-button">
<strong><i class="fa fa-square-check mr-1"></i> Website Preferences</strong> <strong><i class="fa fa-square-check mr-1"></i> Website Preferences</strong>
@ -332,6 +341,204 @@
</form> </form>
</div> </div>
<!-- Look & Feel -->
<div class="card mb-5" id="look">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<i class="fa fa-palette pr-2"></i>
Look &amp; Feel
</p>
</header>
<div class="card-content">
<form method="POST" action="/settings">
<input type="hidden" name="intent" value="look">
{{InputCSRF}}
<!--
-- Profile Header Section
-->
<h2 class="subtitle">Profile Header</h2>
<div class="field">
<label class="label">Preview</label>
</div>
<!-- Header preview hero, should be similar to profile page. -->
<div class="hero is-info is-bold mb-4" id="header-hero-preview">
<div class="hero-body p-4">
<div class="container">
<div class="columns is-mobile is-gapless mt-1 mb-6">
<div class="column is-narrow has-text-centered">
<figure class="profile-photo is-inline-block" style="width: auto; height: auto">
{{template "avatar-48x48" .CurrentUser}}
</figure>
</div>
<div class="column mx-2">
<strong>{{.CurrentUser.NameOrUsername}}</strong>
</div>
{{if and .LoggedIn (not .IsPrivate)}}
<div class="column is-narrow">
<div class="box">
<div style="width: 16px;"></div>
</div>
</div>
{{end}}<!-- if .LoggedIn -->
</div>
</div>
</div>
</div>
<div class="field">
<label class="label">Header Gradient</label>
<div class="columns is-mobile">
<div class="column is-narrow">
<input type="color" class="color"
id="hero-color-start"
name="hero-color-start"
{{if ($User.GetProfileField "hero-color-start")}}
value="{{$User.GetProfileField "hero-color-start"}}"
{{else}}
value="{{template "--prof-colorA"}}"
{{end}}
>
</div>
<div class="column is-narrow">
<input type="color" class="color"
id="hero-color-end"
name="hero-color-end"
{{if ($User.GetProfileField "hero-color-end")}}
value="{{$User.GetProfileField "hero-color-end"}}"
{{else}}
value="{{template "--prof-colorB"}}"
{{end}}
>
</div>
<div class="column">
<label class="checkbox">
<input type="checkbox"
id="hero-text-dark"
name="hero-text-dark"
value="true"
{{if eq (.CurrentUser.GetProfileField "hero-text-dark") "true"}}checked{{end}}
>
Dark text
</label>
</div>
</div>
</div>
<hr>
<!--
-- Profile Page Section
-->
<h2 class="subtitle">Profile Page</h2>
<div class="card block" id="profile-card-preview">
<div class="card-header">
<p class="card-header-title">Card Colors</p>
</div>
<div class="card-body">
<table class="table is-fullwidth">
<tr>
<td>
<strong>Lightness</strong>
</td>
<td>
<div class="select is-fullwidth">
<select id="card-lightness" name="card-lightness">
<option value="">Automatic</option>
<option value="light"{{if eq ($User.GetProfileField "card-lightness") "light"}} selected{{end}}>Light theme (black on white)</option>
<option value="dark"{{if eq ($User.GetProfileField "card-lightness") "dark"}} selected{{end}}>Dark theme (white on black)</option>
</select>
</div>
</td>
</tr>
<tr>
<td class="is-narrow">
<strong>Card Title BG</strong>
</td>
<td>
<input type="color"
id="card-title-bg"
name="card-title-bg"
{{if ($User.GetProfileField "card-title-bg")}}
value="{{$User.GetProfileField "card-title-bg"}}"
{{else}}
value="{{template "--prof-card-bg"}}"
{{end}}
>
</td>
</tr>
<tr>
<td>
<strong>Card Title FG</strong>
</td>
<td>
<input type="color"
id="card-title-fg"
name="card-title-fg"
{{if ($User.GetProfileField "card-title-fg")}}
value="{{$User.GetProfileField "card-title-fg"}}"
{{else}}
value="{{template "--prof-card-fg"}}"
{{end}}
>
</td>
</tr>
<tr>
<td>
<strong>Link Color</strong>
</td>
<td>
<input type="color"
id="card-link-color"
name="card-link-color"
{{if ($User.GetProfileField "card-link-color")}}
value="{{$User.GetProfileField "card-link-color"}}"
{{else}}
value="{{template "--prof-link-fg"}}"
{{end}}
>
<a href="#" onclick="return false" class="ml-2">
Example
<i class="fa fa-link"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
<div class="field">
<label class="label">
Reset Styles
</label>
<label class="checkbox">
<input type="checkbox"
name="reset"
value="true">
Reset to default style
</label>
<p class="help">
If you'd like to reset all your page styles to their default, check
this box and click Save below.
</p>
</div>
<div class="field">
<button type="submit" class="button is-primary">
<i class="fa fa-save mr-2"></i> Save Look &amp; Feel
</button>
</div>
</form>
</div>
</div>
<!-- Website Preferences --> <!-- Website Preferences -->
<div class="card mb-5" id="prefs"> <div class="card mb-5" id="prefs">
<header class="card-header has-background-link"> <header class="card-header has-background-link">
@ -1020,6 +1227,7 @@
window.addEventListener("DOMContentLoaded", (event) => { window.addEventListener("DOMContentLoaded", (event) => {
// The tabs // The tabs
const $profile = document.querySelector("#profile"), const $profile = document.querySelector("#profile"),
$look = document.querySelector("#look"),
$prefs = document.querySelector("#prefs"), $prefs = document.querySelector("#prefs"),
$location = document.querySelector("#location"), $location = document.querySelector("#location"),
$privacy = document.querySelector("#privacy"), $privacy = document.querySelector("#privacy"),
@ -1030,6 +1238,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
// Hide all by default. // Hide all by default.
$profile.style.display = 'none'; $profile.style.display = 'none';
$look.style.display = 'none';
$prefs.style.display = 'none'; $prefs.style.display = 'none';
$location.style.display = 'none'; $location.style.display = 'none';
$privacy.style.display = 'none'; $privacy.style.display = 'none';
@ -1046,6 +1255,9 @@ window.addEventListener("DOMContentLoaded", (event) => {
if (!name) name = "profile"; if (!name) name = "profile";
$activeTab.style.display = 'none'; $activeTab.style.display = 'none';
switch (name) { switch (name) {
case "look":
$activeTab = $look;
break;
case "prefs": case "prefs":
$activeTab = $prefs; $activeTab = $prefs;
break; break;
@ -1073,10 +1285,8 @@ window.addEventListener("DOMContentLoaded", (event) => {
let name_ = tab_.href.split("#").pop(); let name_ = tab_.href.split("#").pop();
if (name !== name_) { if (name !== name_) {
console.log("button: remove is-active", tab_);
tab_.classList.remove("is-active"); tab_.classList.remove("is-active");
} else { } else {
console.log("button %s: ADD is-active", tab_);
tab_.classList.add("is-active"); tab_.classList.add("is-active");
} }
@ -1095,7 +1305,10 @@ window.addEventListener("DOMContentLoaded", (event) => {
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
showTab(name); showTab(name);
e.preventDefault(); if (screen.width >= 1024) {
e.preventDefault();
}
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}); });
@ -1109,6 +1322,68 @@ window.addEventListener("DOMContentLoaded", (event) => {
}); });
}); });
// Look & Feel tab scripts.
window.addEventListener("DOMContentLoaded", (event) => {
let $headerPreview = document.querySelector("#header-hero-preview"),
$colorA = document.querySelector("#hero-color-start"),
$colorB = document.querySelector("#hero-color-end"),
$darkText = document.querySelector("#hero-text-dark"),
$cardPreview = document.querySelector("#profile-card-preview"),
$cardLightness = document.querySelector("#card-lightness"),
$cardTitleBG = document.querySelector("#card-title-bg"),
$cardTitleFG = document.querySelector("#card-title-fg"),
$cardLinkColor = document.querySelector("#card-link-color");
function updatePreview() {
/* Hero banner preview */
let css = `linear-gradient(141deg, ${$colorA.value}, ${$colorB.value})`;
$headerPreview.style.backgroundImage = css;
if ($darkText.checked) {
$headerPreview.classList.remove("has-text-light");
$headerPreview.classList.add("has-text-dark-dark");
} else {
$headerPreview.classList.remove("has-text-dark-dark");
$headerPreview.classList.add("has-text-light");
}
/* Card style preview */
let $table = $cardPreview.querySelector("table"),
$header = $cardPreview.querySelector("div.card-header"),
$title = $cardPreview.querySelector("p.card-header-title"),
$link = $cardPreview.querySelector("a");
$header.style.backgroundColor = $cardTitleBG.value;
$title.style.color = $cardTitleFG.value;
$table.style.backgroundColor = "transparent";
$link.style.color = $cardLinkColor.value;
if ($cardLightness.value === "light") {
$table.style.backgroundColor = "#fff";
$table.style.color = "#4a4a4a";
} else if ($cardLightness.value === "dark") {
$table.style.backgroundColor = "#4a4a4a";
$table.style.color = "#f5f5f5";
}
/* Update <strong> tags in card style table */
($table.querySelectorAll("strong") || []).forEach(node => {
node.style.color = "";
if ($cardLightness.value === "light") {
node.style.color = "#4a4a4a";
} else if ($cardLightness.value === "dark") {
node.style.color = "#f5f5f5";
}
});
}
updatePreview();
([$colorA, $colorB, $darkText, $cardLightness, $cardTitleBG, $cardTitleFG, $cardLinkColor]).forEach(node => {
node.addEventListener("change", (e) => {
updatePreview();
});
});
});
// Location tab scripts. // Location tab scripts.
window.addEventListener("DOMContentLoaded", (event) => { window.addEventListener("DOMContentLoaded", (event) => {
// Get useful controls from the tab. // Get useful controls from the tab.
@ -1270,7 +1545,6 @@ window.addEventListener("DOMContentLoaded", (event) => {
ol.proj.transform(coordinate, 'EPSG:3857', 'EPSG:4326'), ol.proj.transform(coordinate, 'EPSG:3857', 'EPSG:4326'),
2, 2,
); );
console.log("COORDINATE: " + prettyCoord);
// Center the map on the pin. // Center the map on the pin.
if (center) { if (center) {
@ -1301,4 +1575,14 @@ window.addEventListener("DOMContentLoaded", (event) => {
setMapPin(defaultCoords); setMapPin(defaultCoords);
}); });
</script> </script>
<style type="text/css">
/* Ugly hack */
div.hero-body figure.profile-photo {
padding: 2px !important;
}
div.hero-body figure.is-inline-block {
margin-bottom: -6px !important;
}
</style>
{{end}} {{end}}

View File

@ -2,6 +2,9 @@
Notes about {{.User.Username}} Notes about {{.User.Username}}
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<style type="text/css">
{{template "profile-theme-hero-style" .User}}
</style>
<div class="container"> <div class="container">
<section class="hero is-info is-bold"> <section class="hero is-info is-bold">
<div class="hero-body"> <div class="hero-body">

View File

@ -0,0 +1,83 @@
<!-- Theme helpers -->
<!-- Default profile header gradient colors -->
{{define "--prof-colorA"}}#0f81cc{{end}}
{{define "--prof-colorB"}}#7683cc{{end}}
{{define "--prof-card-bg"}}#485fc7{{end}}
{{define "--prof-card-fg"}}#f7f7f7{{end}}
{{define "--prof-link-fg"}}#0099ff{{end}}
{{define "profile-theme-hero-style"}}
{{$colorA := or (.GetProfileField "hero-color-start") "#0f81cc"}}
{{$colorB := or (.GetProfileField "hero-color-end") "#7683cc"}}
section.hero {
background-image: linear-gradient(141deg, {{$colorA}}, {{$colorB}}) !important;
}
.hero h1.title {
color: {{if eq (.GetProfileField "hero-text-dark") "true"}}#4a4a4a{{else}}#f5f5f5{{end}} !important;
}
{{end}}
<!-- Parameter: .User -->
{{define "profile-theme-style"}}
{{$cardTitleBG := or (.GetProfileField "card-title-bg") "#485fc7"}}
{{$cardTitleFG := or (.GetProfileField "card-title-fg") "#f7f7f7"}}
{{$cardLinkFG := or (.GetProfileField "card-link-color") "#0099ff"}}
{{$cardLightness := .GetProfileField "card-lightness"}}
<style type="text/css">
{{template "profile-theme-hero-style" .}}
header.card-header {
background-color: {{$cardTitleBG}} !important;
}
p.card-header-title {
color: {{$cardTitleFG}} !important;
}
.container div.card-content a {
color: {{$cardLinkFG}};
}
.menu-list a {
color: inherit !important;
}
.container div.card-content a:hover {
color: inherit;
}
{{if eq $cardLightness "light"}}
div.box, .container div.card-content, table.table, table.table strong {
background-color: #fff;
color: #4a4a4a;
}
div.tag {
background-color: #ccc;
color: #4a4a4a;
}
/* Slightly less light on dark theme devices */
@media (prefers-color-scheme: dark) {
div.box, .container div.card-content, table.table, table.table strong {
background-color: #e4e4e4;
color: #4a4a4a;
}
}
{{else if eq $cardLightness "dark"}}
div.box, .container div.card-content, table.table, table.table strong {
background-color: #4a4a4a;
color: #f5f5f5;
}
div.tag {
background-color: #333;
color: #f5f5f5;
}
/* Even darker on dark theme devices */
@media (prefers-color-scheme: dark) {
div.box, .container div.card-content, table.table, table.table strong {
background-color: #0a0a0a;
color: #b5b5b5;
}
}
{{end}}
</style>
{{end}}

View File

@ -81,6 +81,11 @@
<!-- Main content template --> <!-- Main content template -->
{{define "content"}} {{define "content"}}
{{if not .IsSiteGallery}}
<style type="text/css">
{{template "profile-theme-hero-style" .User}}
</style>
{{end}}
<div class="container"> <div class="container">
<section class="hero is-info is-bold"> <section class="hero is-info is-bold">
<div class="hero-body"> <div class="hero-body">