Implement basic settings layout

This commit is contained in:
Sung Won Cho 2022-04-17 12:57:46 +10:00
commit 7af65c1eee
13 changed files with 431 additions and 49 deletions

View file

@ -0,0 +1,139 @@
@import './theme';
@import './font';
.settings-page {
.sidebar {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
background: white;
margin-bottom: rem(20px);
margin-top: rem(20px);
@include breakpoint(lg) {
margin-bottom: 0;
margin-top: 0;
}
}
.sidebar-item {
display: block;
padding: rem(12px) rem(16px);
border-left: 4px solid transparent;
@include font-size('regular');
&:hover {
text-decoration: none;
background: $light;
}
&.active {
font-weight: 600;
border-left-color: $first;
}
}
.setting-section-wrapper {
.header {
@include breakpoint(lg) {
display: none;
}
}
.setting-section {
margin-top: rem(24px);
background: white;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.14);
&:first-child {
margin-top: 0;
}
}
.section-heading {
@include font-size('regular');
font-weight: 600;
padding-bottom: rem(4px);
background: $light;
padding: rem(16px) rem(20px);
}
.section-content {
margin-top: rem(20px);
}
.actions {
margin-top: rem(18px);
text-align: right;
}
}
.setting-row {
padding: rem(16px) rem(20px);
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
.setting-row-summary {
display: flex;
flex-direction: column;
align-items: flex-start;
@include breakpoint(md) {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
.setting-row-main {
padding-top: rem(24px);
}
.setting-name {
font-weight: 400;
@include font-size('regular');
margin-bottom: 0;
}
.setting-desc {
margin-bottom: 0;
@include font-size('small');
color: $gray;
}
.setting-action {
display: flex;
flex-direction: column;
@include breakpoint(md) {
flex-direction: row;
}
}
.setting-right {
display: flex;
flex-direction: column;
word-break: break-all;
@include breakpoint(md) {
flex-direction: row;
}
}
.setting-edit {
color: $link;
padding: 0;
&:hover {
color: $link-hover;
}
@include breakpoint(md) {
margin-left: rem(16px);
}
}
.input-row {
& ~ .input-row,
.input-row {
margin-top: rem(12px);
}
}
}
}

View file

@ -32,6 +32,7 @@
@import './home';
@import './note';
@import './books';
@import './settings';
@import './header';
@import './global';

View file

@ -9,4 +9,7 @@ var (
JSFiles = ""
// RootURL is the root url
RootURL = "/"
// Standalone reprsents whether the build is for on-premises. It is a string
// rather than a boolean, so that it can be overridden during compile time.
Standalone = "false"
)

View file

@ -30,12 +30,15 @@ func NewWebRoutes(a *app.App, c *Controllers) []Route {
ret := []Route{
{"GET", "/", mw.Auth(a, c.Users.Settings, redirectGuest), true},
{"GET", "/about", mw.Auth(a, c.Users.About, redirectGuest), true},
{"GET", "/login", mw.GuestOnly(a, c.Users.NewLogin), true},
{"POST", "/login", mw.GuestOnly(a, c.Users.Login), true},
{"POST", "/logout", c.Users.Logout, true},
{"GET", "/password-reset", c.Users.PasswordResetView.ServeHTTP, true},
{"PATCH", "/password-reset", c.Users.PasswordReset, true},
{"PATCH", "/account/profile", c.Users.PasswordReset, true},
{"PATCH", "/account/password", c.Users.PasswordReset, true},
{"GET", "/password-reset/{token}", c.Users.PasswordResetConfirm, true},
{"POST", "/reset-token", c.Users.CreateResetToken, true},
}

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/buildinfo"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
@ -50,9 +51,13 @@ func NewUsers(app *app.App) *Users {
"users/password_reset_confirm",
),
SettingView: views.NewView(app,
views.Config{Title: "Settings", Layout: "base", HelperFuncs: commonHelpers, AlertInBody: true, HeaderTemplate: "navbar"},
views.Config{Layout: "base", HelperFuncs: commonHelpers, AlertInBody: true, HeaderTemplate: "navbar"},
"users/settings",
),
AboutView: views.NewView(app,
views.Config{Title: "About", Layout: "base", HelperFuncs: commonHelpers, AlertInBody: true, HeaderTemplate: "navbar"},
"users/settings_about",
),
app: app,
}
}
@ -62,6 +67,7 @@ type Users struct {
NewView *views.View
LoginView *views.View
SettingView *views.View
AboutView *views.View
PasswordResetView *views.View
PasswordResetConfirmView *views.View
app *app.App
@ -431,3 +437,13 @@ func (u *Users) Settings(w http.ResponseWriter, r *http.Request) {
u.SettingView.Render(w, r, &vd, http.StatusOK)
}
func (u *Users) About(w http.ResponseWriter, r *http.Request) {
vd := views.Data{}
vd.Yield = map[string]interface{}{
"Version": buildinfo.Version,
}
u.AboutView.Render(w, r, &vd, http.StatusOK)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -50,7 +50,7 @@
<ul class="list-unstyled" role="menu">
<li role="none">
<a class="dropdown-link" href="/settings" role="menuitem">Settings</a>
<a class="dropdown-link" href="/" role="menuitem">Settings</a>
</li>
<li role="none">
{{template "logoutForm"}}

View file

@ -0,0 +1,23 @@
{{define "settingsSidebar"}}
<nav class="sidebar">
<ul class="list-unstyled">
<li>
<a class="sidebar-item {{if eq .CurrentPath "/"}}active{{end}}" href="/">Account</a>
</li>
{{if ne .Standalone "true"}}
<li>
<a class="sidebar-item" href="/subscriptions/manage">
Billing
</a>
</li>
{{end}}
<li>
<a class="sidebar-item {{if eq .CurrentPath "/about"}}active{{end}}" href="/about">
About
</a>
</li>
</ul>
</nav>
{{end}}

View file

@ -1,50 +1,180 @@
{{define "yield"}}
<div class="container page page-mobile-full">
<div class="page-header">
<h1 class="page-heading">Settings</h1>
</div>
<div class="page page-mobile-full settings-page">
<div class="container mobile-fw">
<div class="page-header">
<h1 class="page-heading">Settings</h1>
</div>
<div class="row">
<div class="col-12 col-md-12 col-lg-3">
{{template "settingsSidebar" .}}
</div>
<div class="col-12 col-md-12 col-lg-9">
<div class="setting-section-wrapper">
<section class="setting-section">
<h2 class="section-heading">Email</h2>
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Email</h3>
</div>
<div class="setting-right">
{{.Account.Email.String}}
</div>
</div>
</div>
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Email Verified</h3>
</div>
<div class="setting-right">
{{if .Account.EmailVerified}}
Yes
{{else}}
No
{{end}}
</div>
</div>
</div>
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Change Email</h3>
</div>
</div>
<div class="setting-row-main">
{{template "emailForm" .}}
</div>
</div>
</section>
<section class="setting-section">
<h2 class="section-heading">Password</h2>
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Change Password</h3>
<p class="setting-desc">
Set a unique password to protect your data.
</p>
</div>
</div>
<div class="setting-row-main">
{{template "passwordChangeForm" .}}
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{define "settingsSidebar"}}
<nav class="wrapper">
<ul class="list-unstyled">
<li>
<a href="/settings"></a>
<NavLink
class={styles.item}
activeClassName={styles.active}
to={getSettingsPath(SettingSections.account)}
>
Account
</NavLink>
</li>
{__STANDALONE__ ? null : (
<li>
<a class={styles.item} href="/subscriptions/manage">
Billing
</a>
</li>
)}
<li>
<NavLink
class={styles.item}
activeClassName={styles.active}
to={getSettingsPath(SettingSections.notifications)}
>
Notifications
</NavLink>
</li>
<li>
<NavLink
class={styles.item}
activeClassName={styles.active}
to={getSettingsPath(SettingSections.about)}
>
About
</NavLink>
</li>
</ul>
</nav>
{{define "emailForm"}}
<form action="/account/profile">
<input type="password" style="display: none;" readonly />
<div class="input-row">
<label class="input-label" for="email-form-email-input">
New email
</label>
<input
id="email-form-email-input"
type="email"
placeholder="your@email.com"
class="form-control"
autocomplete="new-password"
/>
</div>
<div class="input-row">
<label class="input-label" for="email-form-password-input">
Current password
</label>
<input
id="email-form-password-input"
type="password"
placeholder="********"
class="form-control"
autocomplete="off"
/>
</div>
<div class="actions">
<button
class="button button-first button-normal"
type="submit"
>
Update email
</button>
</div>
</form>
{{end}}
{{define "passwordChangeForm"}}
<form action="/account/password">
<input type="password" style="display: none;" readOnly />
<div class="input-row">
<label class="input-label" for="old-password-input">
Current password
</label>
<input
id="old-password-input"
type="password"
placeholder="********"
class="form-control"
autocomplete="false"
/>
</div>
<div class="input-row">
<label class="input-label" for="new-password-input">
New Password
</label>
<input
id="new-password-input"
type="password"
placeholder="********"
class="form-control"
/>
</div>
<div class="input-row">
<label
class="input-label"
for="new-password-confirmation-input"
>
New Password Confirmation
</label>
<input
id="new-password-confirmation-input"
type="password"
placeholder="********"
class="form-control"
/>
</div>
<div class="actions">
<button
class="button button-first button-normal"
type="submit"
>
Update password
</button>
</div>
</form>
{{end}}

View file

@ -0,0 +1,57 @@
{{define "yield"}}
<div class="page page-mobile-full settings-page">
<div class="container mobile-fw">
<div class="page-header">
<h1 class="page-heading">Settings</h1>
</div>
<div class="row">
<div class="col-12 col-md-12 col-lg-3">
{{template "settingsSidebar" .}}
</div>
<div class="col-12 col-md-12 col-lg-9">
<div class="setting-section-wrapper">
<section class="setting-section">
<h2 class="section-heading">Software</h2>
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Version</h3>
</div>
<div class="setting-right">
{{.Version}}
</div>
</div>
</div>
{{if ne .Standalone "true"}}
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Support</h3>
</div>
<div class="setting-right">
{{if .User.Cloud}}
<a href="mailto:support@getdnote.com">
support@getdnote.com
</a>
{{else}}
Not eligible
{{end}}
</div>
</div>
</div>
{{else}}
{{end}}
</section>
</div>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -10,6 +10,7 @@ import (
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/buildinfo"
"github.com/dnote/dnote/pkg/server/context"
"github.com/dnote/dnote/pkg/server/log"
"github.com/gorilla/csrf"
@ -123,6 +124,15 @@ func (v *View) Render(w http.ResponseWriter, r *http.Request, data *Data, status
vd.User = context.User(r.Context())
vd.Account = context.Account(r.Context())
// Put user data in Yield
if vd.Yield == nil {
vd.Yield = map[string]interface{}{}
}
vd.Yield["Account"] = &vd.Account
vd.Yield["User"] = &vd.User
vd.Yield["CurrentPath"] = r.URL.Path
vd.Yield["Standalone"] = buildinfo.Standalone
var buf bytes.Buffer
csrfField := csrf.TemplateField(r)
tpl := v.Template.Funcs(template.FuncMap{

View file

@ -22,7 +22,7 @@ cp "$basePath"/pkg/server/assets/static/* "$basePath/pkg/server/static"
# run server
moduleName="github.com/dnote/dnote"
ldflags="-X '$moduleName/pkg/server/buildinfo.CSSFiles=main.css' -X '$moduleName/pkg/server/buildinfo.JSFiles=main.js' -X '$moduleName/pkg/server/buildinfo.Version=dev'"
ldflags="-X '$moduleName/pkg/server/buildinfo.CSSFiles=main.css' -X '$moduleName/pkg/server/buildinfo.JSFiles=main.js' -X '$moduleName/pkg/server/buildinfo.Version=dev' -X '$moduleName/pkg/server/buildinfo.Standalone=true'"
task="go run -ldflags \"$ldflags\" main.go start -port 3000"
(