♻️ Refactored web ui with bootstrap and jQuery

This commit is contained in:
LittleSheep 2024-06-02 22:13:41 +08:00
parent 1c36b429ea
commit f1ab0f203f
16 changed files with 543 additions and 1072 deletions

View File

@ -1,12 +1,11 @@
package admin package admin
import ( import (
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func MapAdminEndpoints(A *fiber.App, authFunc utils.AuthFunc) { func MapAdminEndpoints(A *fiber.App, authMiddleware fiber.Handler) {
admin := A.Group("/api/admin").Use(authFunc) admin := A.Group("/api/admin").Use(authMiddleware)
{ {
admin.Post("/badges", grantBadge) admin.Post("/badges", grantBadge)
admin.Delete("/badges/:badgeId", revokeBadge) admin.Delete("/badges/:badgeId", revokeBadge)

View File

@ -144,7 +144,7 @@ func NewServer() {
URL: "/favicon.png", URL: "/favicon.png",
})) }))
admin.MapAdminEndpoints(A, authFunc) admin.MapAdminEndpoints(A, authMiddleware)
ui.MapUserInterface(A, authFunc) ui.MapUserInterface(A, authFunc)
} }

View File

@ -1,51 +0,0 @@
package ui
import (
"fmt"
"html/template"
"time"
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"github.com/gofiber/fiber/v2"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/sujit-baniya/flash"
)
func otherUserinfoPage(c *fiber.Ctx) error {
name := c.Params("account")
var data models.Account
if err := database.C.
Where(&models.Account{Name: name}).
Preload("Profile").
Preload("PersonalPage").
Preload("Contacts").
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
birthday := "Unknown"
if data.Profile.Birthday != nil {
birthday = data.Profile.Birthday.Format(time.RFC822)
}
doc := parser.
NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock).
Parse([]byte(data.PersonalPage.Content))
renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank})
return c.Render("views/users/directory/userinfo", fiber.Map{
"info": flash.Get(c)["message"],
"uid": fmt.Sprintf("%08d", data.ID),
"joined_at": data.CreatedAt.Format(time.RFC822),
"birthday_at": birthday,
"personal_page": template.HTML(markdown.Render(doc, renderer)),
"userinfo": data,
"avatar": data.GetAvatar(),
"banner": data.GetBanner(),
}, "views/layouts/user-center")
}

View File

@ -43,10 +43,5 @@ func MapUserInterface(A *fiber.App, authFunc utils.AuthFunc) {
pages.Post("/mfa/apply", mfaApplyAction) pages.Post("/mfa/apply", mfaApplyAction)
pages.Post("/authorize", authCheckWare, authorizeAction) pages.Post("/authorize", authCheckWare, authorizeAction)
pages.Get("/@:account", otherUserinfoPage)
pages.Get("/users/me", authCheckWare, selfUserinfoPage) pages.Get("/users/me", authCheckWare, selfUserinfoPage)
pages.Get("/users/me/personalize", authCheckWare, personalizePage)
pages.Post("/users/me/personalize", authCheckWare, personalizeAction)
} }

View File

@ -1,101 +0,0 @@
package ui
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/samber/lo"
"github.com/sujit-baniya/flash"
"strings"
"time"
)
func personalizePage(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
localizer := c.Locals("localizer").(*i18n.Localizer)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
Preload("PersonalPage").
Preload("Contacts").
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var birthday any
if data.Profile.Birthday != nil {
birthday = strings.SplitN(data.Profile.Birthday.Format(time.RFC3339), "T", 1)[0]
}
apply, _ := localizer.LocalizeMessage(&i18n.Message{ID: "apply"})
back, _ := localizer.LocalizeMessage(&i18n.Message{ID: "back"})
return c.Render("views/users/personalize", fiber.Map{
"info": flash.Get(c)["message"],
"birthday_at": birthday,
"userinfo": data,
"i18n": fiber.Map{
"apply": apply,
"back": back,
},
}, "views/layouts/user-center")
}
func personalizeAction(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Nick string `form:"nick" validate:"required,min=4,max=24"`
Description string `form:"description"`
FirstName string `form:"first_name"`
LastName string `form:"last_name"`
Birthday string `form:"birthday"`
}
if err := utils.BindAndValidate(c, &data); err != nil {
return flash.WithInfo(c, fiber.Map{
"message": err.Error(),
}).Redirect("/users/me/personalize")
}
var account models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
First(&account).Error; err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("unable to get your userinfo: %v", err),
}).Redirect("/users/me/personalize")
}
account.Nick = data.Nick
account.Description = data.Description
account.Profile.FirstName = data.FirstName
account.Profile.LastName = data.LastName
if birthday, err := time.Parse(time.DateOnly, data.Birthday); err == nil {
account.Profile.Birthday = lo.ToPtr(birthday)
}
if err := database.C.Save(&account).Error; err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("unable to personalize your account: %v", err),
}).Redirect("/users/me/personalize")
} else if err := database.C.Save(&account.Profile).Error; err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("unable to personalize your profile: %v", err),
}).Redirect("/users/me/personalize")
}
services.InvalidAuthCacheWithUser(account.ID)
return flash.WithInfo(c, fiber.Map{
"message": "your account has been personalized",
}).Redirect("/users/me")
}

View File

@ -1,50 +1,56 @@
<div class="left-part"> <div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/> <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
<h1 class="title">{{.i18n.title}} {{.client.Name}}</h1> <h1 class="title">{{.i18n.title}} {{.client.Name}}</h1>
<p class="caption">{{.i18n.caption}}</p> <p class="caption">{{.i18n.caption}}</p>
</div> </div>
<div class="right-part"> <div class="right-part">
<div class="responsive-title-gap "></div> <div class="responsive-title-gap "></div>
<form class="action-form" action="{{.action_url}}" method="POST"> <form class="action-form" action="{{.action_url}}" method="POST">
<div> <div>
<div class="section-title">Description</div> <div class="section-title">Description</div>
<div class="section-body">{{.client.Description}}</div> <div class="section-body">{{.client.Description}}</div>
</div> </div>
<div> <div>
<div class="section-title">Requested scopes</div> <div class="section-title">Requested scopes</div>
<ul class="section-scope"> <ul class="section-scope list-group">
{{range $_, $element := .scopes}} {{range $_, $element := .scopes}}
<li> <li class="monospace list-group-item">
<span class="section-mono">{{$element}}</span> {{$element}}
</li> </li>
{{end}} {{end}}
</ul> </ul>
</div> </div>
<div class="action-form-buttons"> <div class="action-form-buttons">
<md-text-button type="button" id="decline-button">{{.i18n.decline}}</md-text-button> <button class="btn btn-secondary" type="button" id="decline-button">{{.i18n.decline}}</button>
<md-filled-button type="submit">{{.i18n.approve}}</md-filled-button> <button class="btn btn-primary" type="submit">{{.i18n.approve}}</button>
</div> </div>
</form> </form>
</div> </div>
<style> <style>
.section-title { .section-title {
font-weight: bold; font-weight: bold;
} }
.section-mono { .section-scope {
font-family: "Roboto Mono", monospace; margin-top: 4px;
} margin-left: -8px;
margin-right: -8px;
}
.monospace {
font-family: "Roboto Mono", monospace;
}
</style> </style>
<script> <script>
document.getElementById("decline-button").addEventListener("click", () => { $("#decline-button").on("click", () => {
history.back() history.back()
window.close() window.close()
}) })
</script> </script>

View File

@ -4,124 +4,112 @@
{{template "views/partials/header"}} {{template "views/partials/header"}}
<body> <body>
<div class="wrapper-container"> <div class="outer-container">
<div class="wrapper-middleware"> <div class="inner-container">
{{if ne .info nil}} {{if ne .info nil}}
<div class="alert"> <div class="alert alert-primary" role="alert">
<div class="content">{{.info}}</div> <svg class="bi me-2" role="img" aria-label="Info:">
</div> <use xlink:href="#info-fill" />
{{end}} </svg>
<div class="content">{{.info}}</div>
</div>
{{end}}
<div class="wrapper-card"> <div class="card card-container">
{{embed}} {{embed}}
</div> </div>
</div> </div>
</div> </div>
</body> </body>
<style> <style>
.wrapper-container { .outer-container {
width: 100dvw; width: 100dvw;
height: 100dvh; height: 100dvh;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
}
background-color: var(--md-sys-color-surface-container); .inner-container {
width: 100%;
min-width: 0;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card-container {
transition: all .3s;
height: auto;
overflow: auto;
display: grid;
grid-template-columns: 1fr;
justify-content: center;
padding: 48px;
gap: 0 2rem;
}
.logo {
margin-left: -8px;
margin-bottom: -8px;
display: block;
}
.title {
margin-block-start: 0.33em;
margin-block-end: 0.33em;
font-size: 2.5rem;
}
.caption {
font-size: 1rem;
}
.action-form {
display: flex;
flex-direction: column;
gap: 0.8rem 0;
}
.action-form-buttons {
display: flex;
gap: 4px;
margin-top: 10px;
}
.action-form-buttons * {
flex: 1;
}
.block-field {
width: 100%;
}
.responsive-hidden {
display: unset;
}
.columns-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.card-container {
grid-template-columns: 1fr 1fr;
} }
.wrapper-middleware { .responsive-title-gap {
width: 100%; height: calc(56px + 0.44rem);
min-width: 0; display: block;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.wrapper-card {
transition: all .3s;
height: auto;
overflow: auto;
display: grid;
grid-template-columns: 1fr;
justify-content: center;
border-radius: 28px;
padding: 56px;
gap: 0 2rem;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface)
}
.alert {
padding: 16px;
border-radius: 16px;
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
display: flex;
gap: 8px;
}
.alert .content {
flex-grow: 1;
text-transform: capitalize;
}
.logo {
margin-left: -8px;
margin-bottom: -8px;
display: block;
}
.title {
margin-block-start: 0.33em;
margin-block-end: 0.33em;
font-size: 2.5rem;
}
.caption {
font-size: 1rem;
}
.action-form {
display: flex;
flex-direction: column;
gap: 0.8rem 0;
}
.action-form-buttons {
display: flex;
justify-content: end;
margin-top: 8px;
gap: 4px;
}
.block-field {
width: 100%;
}
.responsive-hidden {
display: unset;
}
.columns-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.wrapper-card {
grid-template-columns: 1fr 1fr;
}
.responsive-title-gap {
height: calc(56px + 0.44rem);
display: block;
}
} }
}
</style> </style>
</html>
</html>

View File

@ -4,132 +4,125 @@
{{template "views/partials/header"}} {{template "views/partials/header"}}
<body> <body>
<div class="wrapper-container"> <div class="outer-container">
<div class="wrapper-middleware"> <div class="inner-container">
{{if ne .info nil}} {{if ne .info nil}}
<div class="alert"> <div class="alert alert-primary" role="alert">
<div class="content">{{.info}}</div> <svg class="bi me-2" role="img" aria-label="Info:">
</div> <use xlink:href="#info-fill" />
{{end}} </svg>
<div class="content">{{.info}}</div>
</div>
{{end}}
<div class="wrapper-card"> <div class="card card-container">
{{embed}} {{embed}}
</div> </div>
</div> </div>
</div> </div>
</body> </body>
<style> <style>
.wrapper-container { body,
width: 100dvw; .outer-container {
min-height: 100dvh; scrollbar-width: none;
display: flex; overflow-x: hidden;
justify-content: center; }
align-items: center;
overflow: auto;
scrollbar-width: none; .outer-container {
width: 100dvw;
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
}
background-color: var(--md-sys-color-surface-container); .outer-container::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none;
width: 0;
}
.inner-container {
width: 100%;
min-width: 0;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
padding-top: 1rem;
padding-bottom: 1rem;
}
.card-container {
transition: all .3s;
height: auto;
overflow: auto;
display: grid;
grid-template-columns: 1fr;
justify-content: center;
padding: 48px;
gap: 0 2rem;
}
.logo {
margin-left: -8px;
margin-bottom: -8px;
display: block;
}
.title {
margin-block-start: 0.33em;
margin-block-end: 0.33em;
font-size: 2.5rem;
}
.caption {
font-size: 1rem;
}
.action-form {
display: flex;
flex-direction: column;
gap: 0.8rem 0;
}
.action-form-buttons {
display: flex;
justify-content: end;
margin-top: 8px;
gap: 4px;
}
.block-field {
width: 100%;
}
.responsive-hidden {
display: unset;
}
.columns-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.card-container {
grid-template-columns: 1fr 1fr;
} }
.wrapper-container::-webkit-scrollbar, body::-webkit-scrollbar { .responsive-title-gap {
display: none; height: calc(56px + 0.44rem);
width: 0; display: block;
}
.wrapper-middleware {
width: 100%;
min-width: 0;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.wrapper-card {
transition: all .3s;
height: auto;
overflow: auto;
display: grid;
grid-template-columns: 1fr;
justify-content: center;
border-radius: 28px;
padding: 56px;
gap: 2rem;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface)
}
.alert {
padding: 16px;
border-radius: 16px;
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
display: flex;
gap: 8px;
}
.alert .content {
flex-grow: 1;
text-transform: capitalize;
}
.logo {
margin-left: -8px;
margin-bottom: -8px;
display: block;
}
.title {
margin-block-start: 0.33em;
margin-block-end: 0.33em;
font-size: 2.5rem;
}
.caption {
font-size: 1rem;
}
.action-form {
display: flex;
flex-direction: column;
gap: 0.8rem 0;
}
.action-form-buttons {
display: flex;
justify-content: end;
margin-top: 8px;
gap: 4px;
}
.block-field {
width: 100%;
}
.responsive-hidden {
display: unset;
}
.columns-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.wrapper-card {
grid-template-columns: 1fr 1fr;
}
.responsive-title-gap {
height: calc(56px + 0.44rem);
display: block;
}
} }
}
</style> </style>
</html> </html>

View File

@ -1,47 +1,43 @@
<div class="left-part"> <div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/> <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
<h1 class="title">{{.i18n.title}}</h1> <h1 class="title">{{.i18n.title}}</h1>
<p class="caption">{{.i18n.caption}}</p> <p class="caption">{{.i18n.caption}}</p>
</div> </div>
<div class="right-part"> <div class="right-part">
<div class="responsive-title-gap"></div> <div class="responsive-title-gap"></div>
<form class="action-form" action="/mfa/apply" method="POST"> <form class="action-form" action="/mfa/apply" method="POST">
<label> <label>
<input name="ticket_id" value="{{.ticket_id}}" hidden> <input name="ticket_id" value="{{.ticket_id}}" hidden>
</label> </label>
<label> <label>
<input name="factor_id" value="{{.factor_id}}" hidden> <input name="factor_id" value="{{.factor_id}}" hidden>
</label> </label>
<div class="factor-label">{{.label}}</div> <div class="factor-label">{{.label}}</div>
<md-outlined-text-field <div class="mb-1 block-field">
class="block-field" <label for="code" class="form-label">{{.i18n.password}}</label>
name="code" <input type="password" class="form-control" id="code" name="password" autocomplete="off">
type="password" </div>
autocomplete="off"
label={{.i18n.password}}
>
</md-outlined-text-field>
<div class="action-form-buttons"> <div class="action-form-buttons">
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button> <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
</div> </div>
</form> </form>
</div> </div>
<style> <style>
.factor-label { .factor-label {
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.factor-label { .factor-label {
text-align: center; text-align: center;
}
} }
</style> }
</style>

View File

@ -1,61 +1,59 @@
<div class="left-part"> <div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/> <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
<h1 class="title">{{.i18n.title}}</h1> <h1 class="title">{{.i18n.title}}</h1>
<p class="caption">{{.i18n.caption}}</p> <p class="caption">{{.i18n.caption}}</p>
</div> </div>
<div class="right-part"> <div class="right-part">
<div class="responsive-title-gap"></div> <div class="responsive-title-gap"></div>
<form class="action-form" action="/mfa" method="POST"> <form class="action-form" action="/mfa" method="POST">
<label> <label>
<input name="ticket_id" value="{{.ticket_id}}" hidden> <input name="ticket_id" value="{{.ticket_id}}" hidden>
</label> </label>
{{if ne .redirect_uri nil}} {{if ne .redirect_uri nil}}
<label> <label>
<input name="redirect_uri" value="{{.redirect_uri}}" hidden> <input name="redirect_uri" value="{{.redirect_uri}}" hidden>
</label> </label>
{{end}} {{end}}
<div class="block-field factor-list" role="radiogroup"> <div class="block-field factor-list" role="radiogroup">
{{range $_, $element := .factors}} {{range $_, $element := .factors}}
<div class="factor-label"> <div class="factor-label">
<md-radio <div class="form-check">
aria-label="{{$element.name}}" <input class="form-check-input" type="radio" name="factor_id" id="factor-{{$element.id}}"
id="factor-{{$element.id}}" value="{{$element.id}}">
value="{{$element.id}}" <label class="form-check-label" for="factor-{{$element.id}}">
touch-target="wrapper" {{$element.name}}
name="factor_id" </label>
>
</md-radio>
<label for="factor-{{$element.id}}">{{$element.name}}</label>
</div>
{{end}}
</div> </div>
</div>
{{end}}
</div>
<div class="action-form-buttons"> <div class="action-form-buttons">
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button> <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
</div> </div>
</form> </form>
</div> </div>
<style> <style>
.factor-list { .factor-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.factor-label { .factor-label {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.factor-label label { .factor-label label {
display: inline-flex; display: inline-flex;
place-items: center; place-items: center;
gap: 8px; gap: 8px;
font-family: Roboto, system-ui; font-family: Roboto, system-ui;
color: var(--md-sys-color-on-background); color: var(--md-sys-color-on-background);
} }
</style> </style>

View File

@ -1,89 +1,56 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" <meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
rel="stylesheet"
/>
<script type="importmap"> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
{ integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
"imports": { crossorigin="anonymous"></script>
"@material/web/": "https://esm.run/@material/web/" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
} integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
} crossorigin="anonymous"></script>
</script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script type="module">
import "@material/web/all.js";
import {styles as typescaleStyles} from "@material/web/typography/md-typescale-styles.js";
document.adoptedStyleSheets.push(typescaleStyles.styleSheet); <svg xmlns="http://www.w3.org/2000/svg" class="d-none">
</script> <symbol id="info-fill" viewBox="0 0 16 16">
<path
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" />
</symbol>
</svg>
<title>Solarpass</title> <title>Solarpass</title>
<style> <style>
:root, :host { html,
--md-sys-color-background: #fbf8ff; body {
--md-sys-color-on-background: #1b1b20; padding: 0;
--md-sys-color-surface: #fbf8ff; margin: 0;
--md-sys-color-surface-dim: #dcd9df; }
--md-sys-color-surface-bright: #fbf8ff;
--md-sys-color-surface-container-lowest: #ffffff;
--md-sys-color-surface-container-low: #f6f2f9;
--md-sys-color-surface-container: #f0edf3;
--md-sys-color-surface-container-high: #eae7ed;
--md-sys-color-surface-container-highest: #e4e1e8;
--md-sys-color-on-surface: #1b1b20;
--md-sys-color-surface-variant: #e3e1ee;
--md-sys-color-on-surface-variant: #464650;
--md-sys-color-inverse-surface: #303035;
--md-sys-color-inverse-on-surface: #f3eff6;
--md-sys-color-outline: #777681;
--md-sys-color-outline-variant: #c7c5d2;
--md-sys-color-shadow: #000000;
--md-sys-color-scrim: #000000;
--md-sys-color-surface-tint: #53589d;
--md-sys-color-primary: #373c7e;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: #5b60a5;
--md-sys-color-on-primary-container: #ffffff;
--md-sys-color-inverse-primary: #bec2ff;
--md-sys-color-secondary: #5b5c79;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-secondary-container: #e2e1ff;
--md-sys-color-on-secondary-container: #454662;
--md-sys-color-tertiary: #662d5e;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-tertiary-container: #8e5084;
--md-sys-color-on-tertiary-container: #ffffff;
--md-sys-color-error: #ba1a1a;
--md-sys-color-on-error: #ffffff;
--md-sys-color-error-container: #ffdad6;
--md-sys-color-on-error-container: #410002;
}
.material-symbols-outlined { .alert {
font-variation-settings: padding: 16px 48px;
'FILL' 0, display: flex;
'wght' 400, align-items: center;
'GRAD' 0, gap: 8px;
'opsz' 24 }
}
html, body { .alert .bi {
padding: 0; aspect-ratio: 1;
margin: 0; width: 16px;
background-color: var(--md-sys-color-surface-container); fill: var(--bs-alert-color);
} }
</style>
.alert .content {
flex-grow: 1;
text-transform: capitalize;
}
</style>
</head> </head>

View File

@ -1,35 +1,27 @@
<div class="left-part"> <div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/> <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
<h1 class="title">{{.i18n.title}}</h1> <h1 class="title">{{.i18n.title}}</h1>
<p class="caption">{{.i18n.caption}}</p> <p class="caption">{{.i18n.caption}}</p>
</div> </div>
<div class="right-part"> <div class="right-part">
<div class="responsive-title-gap"></div> <div class="responsive-title-gap"></div>
<form class="action-form" action="/sign-in" method="POST"> <form class="action-form" action="/sign-in" method="POST">
<md-outlined-text-field <div class="mb-1 block-field">
class="block-field" <label for="username" class="form-label">{{.i18n.username}}</label>
name="username" <input type="text" class="form-control" id="username" name="username">
type="text" </div>
autocomplete="username"
label={{.i18n.username}}
>
</md-outlined-text-field>
<md-outlined-text-field <div class="mb-1 block-field">
class="block-field" <label for="password" class="form-label">{{.i18n.password}}</label>
name="password" <input type="password" class="form-control" id="password" name="password">
type="password" </div>
autocomplete="password"
label={{.i18n.password}}
>
</md-outlined-text-field>
<div class="action-form-buttons"> <div class="action-form-buttons">
<md-text-button type="button" href="/sign-up">{{.i18n.signup}}</md-text-button> <a class="btn btn-secondary" href="/sign-up">{{.i18n.signup}}</a>
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button> <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,67 +1,47 @@
<div class="left-part"> <div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/> <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
<h1 class="title">{{.i18n.title}}</h1> <h1 class="title">{{.i18n.title}}</h1>
<p class="caption">{{.i18n.caption}}</p> <p class="caption">{{.i18n.caption}}</p>
</div> </div>
<div class="right-part"> <div class="right-part">
<div class="responsive-title-gap"></div> <div class="responsive-title-gap"></div>
<form class="action-form" action="/sign-up" method="POST"> <form class="action-form" action="/sign-up" method="POST">
<div class="columns-two"> <div class="columns-two">
<md-outlined-text-field <div class="mb-1">
class="block-field" <label for="name" class="form-label">{{.i18n.username}}</label>
name="name" <input type="text" class="form-control" id="name" name="name">
type="text" </div>
autocomplete="username"
label={{.i18n.username}}
>
</md-outlined-text-field>
<md-outlined-text-field <div class="mb-1">
class="block-field" <label for="nick" class="form-label">{{.i18n.nickname}}</label>
name="nick" <input type="text" class="form-control" id="nick" name="nick">
type="text" </div>
autocomplete="nickname" </div>
label={{.i18n.nickname}}
>
</md-outlined-text-field>
</div>
<md-outlined-text-field <div class="mb-1 block-field">
class="block-field" <label for="email" class="form-label">{{.i18n.email}}</label>
name="email" <input type="email" class="form-control" id="email" name="email">
type="email" </div>
autocomplete="email"
label={{.i18n.email}}
>
</md-outlined-text-field>
<md-outlined-text-field <div class="mb-1">
class="block-field" <label for="password" class="form-label">{{.i18n.password}}</label>
name="password" <input type="password" class="form-control" id="password" name="password" autocomplete="new-password">
type="password" </div>
autocomplete="new-password"
label={{.i18n.password}}
>
</md-outlined-text-field>
{{if eq .use_magic_token true}} {{if eq .use_magic_token true}}
<md-outlined-text-field <div class="mb-1">
class="block-field" <label for="token" class="form-label">{{.i18n.password}}</label>
name="magic_token" <input type="password" class="form-control" id="token" name="magic_token" autocomplete="new-password">
type="password" </div>
autocomplete="off" {{end}}
label={{.i18n.magic_token}}
>
</md-outlined-text-field>
{{end}}
<div class="action-form-buttons"> <div class="action-form-buttons">
<md-text-button type="button" href="/sign-in">{{.i18n.signin}}</md-text-button> <a class="btn btn-secondary" href="/sign-in">{{.i18n.signin}}</a>
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button> <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,165 +0,0 @@
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/base.min.css">
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/components.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tailwindcss/typography@0.1.2/dist/typography.min.css">
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/utilities.min.css">
<div class="banner-container">
{{if ne .userinfo.Banner nil}}
<img src="{{.banner}}" alt="Banner" class="banner">
{{end}}
</div>
<div class="left-part name-card">
{{if ne .userinfo.Avatar nil}}
<img src="{{.avatar}}" alt="Avatar" class="avatar">
{{else}}
<div class="avatar empty">
<span class="material-symbols-outlined">account_circle</span>
</div>
{{end}}
<div class="name">
<h2 class="username">{{.userinfo.Nick}}</h2>
<h6 class="nickname">@{{.userinfo.Name}}</h6>
</div>
{{if gt (len .userinfo.Description) 0}}
<div class="description">{{.userinfo.Description}}</div>
{{else}}
<div class="description empty">No description yet.</div>
{{end}}
<div class="uid">#{{.uid}}</div>
<div class="metadata">
<div><span class="material-symbols-outlined">calendar_month</span> {{.joined_at}}</div>
<div><span class="material-symbols-outlined">cake</span> {{.birthday_at}}</div>
</div>
<div class="actions">
<md-filled-tonal-button disabled class="action">
Add as friend
<span slot="icon" class="material-symbols-outlined">group_add</span>
</md-filled-tonal-button>
</div>
</div>
<div class="right-part">
<article class="personal-page prose">
{{.personal_page}}
</article>
</div>
<style>
.avatar {
display: block;
width: 64px;
height: 64px;
object-fit: cover;
clip-path: circle();
}
.avatar.empty {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
@media (min-width: 768px) {
.banner-container {
grid-column: span 2;
}
}
.banner {
display: block;
object-fit: cover;
border-radius: 28px;
aspect-ratio: 3 / 1;
width: 100%;
}
.name-card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.name-card .name {
display: flex;
align-items: baseline;
gap: 0.3rem;
}
.name-card .username {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.name-card .nickname {
margin: 0;
font-size: 0.75rem;
font-weight: 500;
}
.name-card .uid {
margin-top: -0.8rem;
font-size: 0.7rem;
font-weight: 400;
font-family: Roboto Mono, monospace;
}
.name-card .description {
margin-top: -1.25rem;
}
.description.empty {
font-style: italic;
}
.name-card .metadata {
font-size: 0.85rem;
font-weight: 500;
display: flex;
flex-direction: column;
}
.metadata > div {
display: flex;
align-items: center;
gap: 0.25rem;
}
.metadata .material-symbols-outlined {
font-size: 1rem;
display: block;
}
.actions {
display: flex;
gap: 0.5rem;
margin: 0 -0.5rem;
}
@media (min-width: 768px) {
.actions {
flex-direction: column;
}
}
.actions .action {
width: fit-content;
}
.actions .material-symbols-outlined {
font-size: 20px;
margin-bottom: 4px;
}
.left-part .prose {
min-width: 0;
max-width: unset;
}
</style>

View File

@ -4,166 +4,150 @@
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/utilities.min.css"> <link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/utilities.min.css">
<div class="banner-container"> <div class="banner-container">
{{if ne .userinfo.Banner nil}} {{if ne .userinfo.Banner nil}}
<img src="{{.banner}}" alt="Banner" class="banner"> <img src="{{.banner}}" alt="Banner" class="banner">
{{end}} {{end}}
</div> </div>
<div class="left-part name-card"> <div class="left-part name-card">
{{if ne .userinfo.Avatar nil}} {{if ne .userinfo.Avatar nil}}
<img src="{{.avatar}}" alt="Avatar" class="avatar"> <img src="{{.avatar}}" alt="Avatar" class="avatar">
{{else}} {{else}}
<div class="avatar empty"> <div class="avatar empty">
<span class="material-symbols-outlined">account_circle</span> <span class="material-symbols-outlined">account_circle</span>
</div> </div>
{{end}} {{end}}
<div class="name"> <div class="name">
<h2 class="username">{{.userinfo.Nick}}</h2> <h2 class="username">{{.userinfo.Nick}}</h2>
<h6 class="nickname">@{{.userinfo.Name}}</h6> <h6 class="nickname">@{{.userinfo.Name}}</h6>
</div> </div>
{{if gt (len .userinfo.Description) 0}} {{if gt (len .userinfo.Description) 0}}
<div class="description">{{.userinfo.Description}}</div> <div class="description">{{.userinfo.Description}}</div>
{{else}} {{else}}
<div class="description empty">No description yet.</div> <div class="description empty">No description yet.</div>
{{end}} {{end}}
<div class="uid">#{{.uid}}</div> <div class="uid">#{{.uid}}</div>
<div class="metadata">
<div><span class="material-symbols-outlined">calendar_month</span> {{.joined_at}}</div>
<div><span class="material-symbols-outlined">cake</span> {{.birthday_at}}</div>
</div>
<div class="actions">
<md-filled-tonal-button class="action" href="/users/me/personalize">
Personalize
<span slot="icon" class="material-symbols-outlined">palette</span>
</md-filled-tonal-button>
<md-filled-tonal-button disabled class="action" href="/users/me/security">
Security
<span slot="icon" class="material-symbols-outlined">security</span>
</md-filled-tonal-button>
</div>
</div> </div>
<div class="right-part"> <div class="right-part">
<article class="personal-page prose"> <article class="personal-page prose">
{{.personal_page}} {{.personal_page}}
</article> </article>
</div> </div>
<style> <style>
.avatar { .avatar {
display: block; display: block;
width: 64px; width: 64px;
height: 64px; height: 64px;
object-fit: cover; object-fit: cover;
clip-path: circle(); clip-path: circle();
}
.avatar.empty {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
@media (min-width: 768px) {
.banner-container {
grid-column: span 2;
} }
}
.avatar.empty { .banner {
display: flex; display: block;
justify-content: center; object-fit: cover;
align-items: center; border-radius: 28px;
background-color: var(--md-sys-color-secondary); aspect-ratio: 3 / 1;
color: var(--md-sys-color-on-secondary); width: 100%;
} }
@media (min-width: 768px) { .name-card {
.banner-container { display: flex;
grid-column: span 2; flex-direction: column;
} gap: 1rem;
} }
.banner { .name-card .name {
display: block; display: flex;
object-fit: cover; align-items: baseline;
border-radius: 28px; gap: 0.3rem;
aspect-ratio: 3 / 1; }
width: 100%;
}
.name-card { .name-card .username {
display: flex; margin: 0;
flex-direction: column; font-size: 1.5rem;
gap: 1rem; font-weight: 600;
} }
.name-card .name { .name-card .nickname {
display: flex; margin: 0;
align-items: baseline; font-size: 0.75rem;
gap: 0.3rem; font-weight: 500;
} }
.name-card .username { .name-card .uid {
margin: 0; margin-top: -0.8rem;
font-size: 1.5rem; font-size: 0.7rem;
font-weight: 600; font-weight: 400;
} font-family: Roboto Mono, monospace;
}
.name-card .nickname { .name-card .description {
margin: 0; margin-top: -1.25rem;
font-size: 0.75rem; }
font-weight: 500;
}
.name-card .uid { .description.empty {
margin-top: -0.8rem; font-style: italic;
font-size: 0.7rem; }
font-weight: 400;
font-family: Roboto Mono, monospace;
}
.name-card .description { .name-card .metadata {
margin-top: -1.25rem; font-size: 0.85rem;
} font-weight: 500;
display: flex;
flex-direction: column;
}
.description.empty { .metadata>div {
font-style: italic; display: flex;
} align-items: center;
gap: 0.25rem;
}
.name-card .metadata { .metadata .material-symbols-outlined {
font-size: 0.85rem; font-size: 1rem;
font-weight: 500; display: block;
display: flex; }
flex-direction: column;
}
.metadata > div { .actions {
display: flex; display: flex;
align-items: center; gap: 0.5rem;
gap: 0.25rem; margin: 0 -0.5rem;
} }
.metadata .material-symbols-outlined {
font-size: 1rem;
display: block;
}
@media (min-width: 768px) {
.actions { .actions {
display: flex; flex-direction: column;
gap: 0.5rem;
margin: 0 -0.5rem;
} }
}
@media (min-width: 768px) { .actions .action {
.actions { width: fit-content;
flex-direction: column; }
}
}
.actions .action { .actions .material-symbols-outlined {
width: fit-content; font-size: 20px;
} margin-bottom: 4px;
}
.actions .material-symbols-outlined { .left-part .prose {
font-size: 20px; min-width: 0;
margin-bottom: 4px; max-width: unset;
} }
</style>
.left-part .prose {
min-width: 0;
max-width: unset;
}
</style>

View File

@ -1,110 +0,0 @@
<div class="left-part">
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/>
<h1 class="title">Personalize</h1>
<p class="caption">Personalize your account, and make us provide better service for you.</p>
</div>
<div class="right-part">
<div class="responsive-title-gap"></div>
<div class="personalize-actions">
<span>We doesn't support edit avatar / banner through Hydrogen.Passport web yet. Go try our Solian App!</span>
</div>
<form class="action-form" action="/users/me/personalize" method="POST">
<div class="columns-two">
<md-outlined-text-field
class="block-field"
name="name"
type="text"
autocomplete="username"
label="Username"
disabled
value="{{.userinfo.Name}}"
>
</md-outlined-text-field>
<md-outlined-text-field
class="block-field"
name="nick"
type="text"
autocomplete="nickname"
label="Nickname"
value="{{.userinfo.Nick}}"
>
</md-outlined-text-field>
</div>
<div class="columns-two">
<md-outlined-text-field
class="block-field"
name="first_name"
type="text"
autocomplete="given_name"
label="First Name"
value="{{if gt (len .userinfo.Profile.FirstName) 0}}{{.userinfo.Profile.FirstName}}{{end}}"
>
</md-outlined-text-field>
<md-outlined-text-field
class="block-field"
name="last_name"
type="text"
autocomplete="family_name"
label="Last Name"
value="{{if gt (len .userinfo.Profile.LastName) 0}}{{.userinfo.Profile.LastName}}{{end}}"
>
</md-outlined-text-field>
</div>
<md-outlined-text-field
class="block-field"
name="birthday"
type="date"
label="Birthday"
value="{{if ne .birthday nil}}{{.birthday}}{{end}}"
>
</md-outlined-text-field>
<md-outlined-text-field
class="block-field"
name="description"
type="textarea"
autocomplete="off"
label="Description"
value="{{if gt (len .userinfo.Description) 0}}{{.userinfo.Description}}{{end}}"
style="resize: vertical"
>
</md-outlined-text-field>
<div class="action-form-buttons">
<md-text-button type="button" href="/users/me">{{.i18n.back}}</md-text-button>
<md-filled-button type="submit">{{.i18n.apply}}</md-filled-button>
</div>
</form>
</div>
<style>
.input-label {
font-size: 14px;
text-align: left;
}
.personalize-actions {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
margin-left: -0.2rem;
margin-right: -0.2rem;
}
.personalize-actions .personalize-action {
width: fit-content;
}
.personalize-action .material-symbols-outlined {
font-size: 20px;
margin-bottom: 2px;
}
</style>