adattato html, test htmx con componente svelte
This commit is contained in:
parent
0cd6ce05cd
commit
83e85bf899
34
.env.example
34
.env.example
|
|
@ -1,34 +0,0 @@
|
||||||
# App
|
|
||||||
APP_NAME=trustcontact
|
|
||||||
APP_ENV=develop
|
|
||||||
APP_PORT=3000
|
|
||||||
APP_BASE_URL=http://localhost:3000
|
|
||||||
APP_BUILD_HASH=dev
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_DRIVER=sqlite
|
|
||||||
DB_SQLITE_PATH=data/app.sqlite3
|
|
||||||
DB_POSTGRES_DSN=
|
|
||||||
DB_PG_DSN=
|
|
||||||
|
|
||||||
# CORS (comma-separated)
|
|
||||||
CORS_ORIGINS=http://localhost:3000
|
|
||||||
CORS_HEADERS=Origin,Content-Type,Accept,Authorization,HX-Request
|
|
||||||
CORS_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
|
||||||
CORS_CREDENTIALS=true
|
|
||||||
|
|
||||||
# Sessions
|
|
||||||
SESSION_KEY=change-me-in-prod
|
|
||||||
|
|
||||||
# SMTP / Email sink
|
|
||||||
SMTP_HOST=localhost
|
|
||||||
SMTP_PORT=1025
|
|
||||||
SMTP_USERNAME=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=noreply@example.test
|
|
||||||
SMTP_FROM_NAME=Trustcontact
|
|
||||||
EMAIL_SINK_DIR=data/emails
|
|
||||||
|
|
||||||
# Flags
|
|
||||||
AUTO_MIGRATE=true
|
|
||||||
SEED_ENABLED=false
|
|
||||||
|
|
@ -3,17 +3,13 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
"trustcontact/internal/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminController struct {
|
type AdminController struct{}
|
||||||
usersService *services.UsersService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdminController(usersService *services.UsersService) *AdminController {
|
func NewAdminController() *AdminController {
|
||||||
return &AdminController{usersService: usersService}
|
return &AdminController{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
|
func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
|
||||||
|
|
@ -36,36 +32,3 @@ func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return executeLayout(c, tmpl, viewData)
|
return executeLayout(c, tmpl, viewData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AdminController) Users(c *fiber.Ctx) error {
|
|
||||||
pageData, err := ac.usersService.List(services.UsersQuery{
|
|
||||||
Q: c.Query("q"),
|
|
||||||
Sort: c.Query("sort", "id"),
|
|
||||||
Dir: c.Query("dir", "asc"),
|
|
||||||
Page: parseIntOrDefault(c.Query("page"), 1),
|
|
||||||
PageSize: parseIntOrDefault(c.Query("pageSize"), 20),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
viewData := map[string]any{
|
|
||||||
"Title": "Admin Users",
|
|
||||||
"NavSection": "admin",
|
|
||||||
"PageData": pageData,
|
|
||||||
}
|
|
||||||
for k, v := range localsTemplateData(c) {
|
|
||||||
viewData[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(
|
|
||||||
"web/templates/layout.html",
|
|
||||||
"web/templates/public/_flash.html",
|
|
||||||
"web/templates/admin/users.html",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return executeLayout(c, tmpl, viewData)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,23 @@ func NewAuthController(authService *services.AuthService) *AuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ShowHome(c *fiber.Ctx) error {
|
func (ac *AuthController) ShowHome(c *fiber.Ctx) error {
|
||||||
|
if _, ok := httpmw.CurrentUserFromContext(c); ok {
|
||||||
|
return c.Redirect("/welcome")
|
||||||
|
}
|
||||||
|
|
||||||
return renderPublic(c, "home.html", map[string]any{
|
return renderPublic(c, "home.html", map[string]any{
|
||||||
"Title": "Home",
|
"Title": "Home",
|
||||||
"NavSection": "public",
|
"NavSection": "public",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ac *AuthController) ShowWelcome(c *fiber.Ctx) error {
|
||||||
|
return renderPrivate(c, "welcome.html", map[string]any{
|
||||||
|
"Title": "Welcome",
|
||||||
|
"NavSection": "private",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ShowSignup(c *fiber.Ctx) error {
|
func (ac *AuthController) ShowSignup(c *fiber.Ctx) error {
|
||||||
return renderPublic(c, "signup.html", map[string]any{
|
return renderPublic(c, "signup.html", map[string]any{
|
||||||
"Title": "Sign up",
|
"Title": "Sign up",
|
||||||
|
|
@ -90,7 +101,7 @@ func (ac *AuthController) Login(c *fiber.Ctx) error {
|
||||||
if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil {
|
if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.Redirect("/private")
|
return c.Redirect("/welcome")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) Logout(c *fiber.Ctx) error {
|
func (ac *AuthController) Logout(c *fiber.Ctx) error {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,42 @@ func renderPublic(c *fiber.Ctx, page string, data map[string]any) error {
|
||||||
return c.Send(out.Bytes())
|
return c.Send(out.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderPrivate(c *fiber.Ctx, page string, data map[string]any) error {
|
||||||
|
viewData := map[string]any{}
|
||||||
|
for k, v := range localsTemplateData(c) {
|
||||||
|
viewData[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range data {
|
||||||
|
viewData[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := viewData["Title"]; !ok {
|
||||||
|
viewData["Title"] = "Trustcontact"
|
||||||
|
}
|
||||||
|
if _, ok := viewData["NavSection"]; !ok {
|
||||||
|
viewData["NavSection"] = "private"
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{
|
||||||
|
"web/templates/layout.html",
|
||||||
|
"web/templates/public/_flash.html",
|
||||||
|
filepath.Join("web/templates/private", page),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := tmpl.ExecuteTemplate(&out, "layout.html", viewData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Type("html", "utf-8")
|
||||||
|
return c.Send(out.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
func localsTemplateData(c *fiber.Ctx) map[string]any {
|
func localsTemplateData(c *fiber.Ctx) map[string]any {
|
||||||
data, ok := c.Locals("template_data").(map[string]any)
|
data, ok := c.Locals("template_data").(map[string]any)
|
||||||
if !ok || data == nil {
|
if !ok || data == nil {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ func (uc *UsersController) Index(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewData := map[string]any{
|
viewData := map[string]any{
|
||||||
"Title": "Users",
|
"Title": "Admin Users",
|
||||||
"NavSection": "private",
|
"NavSection": "admin",
|
||||||
"PageData": pageData,
|
"PageData": pageData,
|
||||||
}
|
}
|
||||||
for k, v := range localsTemplateData(c) {
|
for k, v := range localsTemplateData(c) {
|
||||||
|
|
@ -39,8 +39,8 @@ func (uc *UsersController) Index(c *fiber.Ctx) error {
|
||||||
tmpl, err := template.ParseFiles(
|
tmpl, err := template.ParseFiles(
|
||||||
"web/templates/layout.html",
|
"web/templates/layout.html",
|
||||||
"web/templates/public/_flash.html",
|
"web/templates/public/_flash.html",
|
||||||
"web/templates/private/users/index.html",
|
"web/templates/admin/users/index.html",
|
||||||
"web/templates/private/users/_table.html",
|
"web/templates/admin/users/_table.html",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -56,7 +56,7 @@ func (uc *UsersController) Table(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewData := map[string]any{"PageData": pageData}
|
viewData := map[string]any{"PageData": pageData}
|
||||||
tmpl, err := template.ParseFiles("web/templates/private/users/_table.html")
|
tmpl, err := template.ParseFiles("web/templates/admin/users/_table.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ func (uc *UsersController) Modal(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewData := map[string]any{"User": user}
|
viewData := map[string]any{"User": user}
|
||||||
tmpl, err := template.ParseFiles("web/templates/private/users/_modal.html")
|
tmpl, err := template.ParseFiles("web/templates/admin/users/_modal.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error {
|
func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error {
|
||||||
|
app.Static("/static", "web/static")
|
||||||
|
|
||||||
app.Use(httpmw.SessionStoreMiddleware(store))
|
app.Use(httpmw.SessionStoreMiddleware(store))
|
||||||
app.Use(httpmw.CurrentUserMiddleware(store, database))
|
app.Use(httpmw.CurrentUserMiddleware(store, database))
|
||||||
app.Use(httpmw.ConsumeFlash())
|
app.Use(httpmw.ConsumeFlash())
|
||||||
|
|
@ -28,7 +30,7 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||||
authController := controllers.NewAuthController(authService)
|
authController := controllers.NewAuthController(authService)
|
||||||
usersService := services.NewUsersService(database)
|
usersService := services.NewUsersService(database)
|
||||||
usersController := controllers.NewUsersController(usersService)
|
usersController := controllers.NewUsersController(usersService)
|
||||||
adminController := controllers.NewAdminController(usersService)
|
adminController := controllers.NewAdminController()
|
||||||
|
|
||||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
|
@ -46,18 +48,18 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||||
app.Post("/forgot-password", authController.ForgotPassword)
|
app.Post("/forgot-password", authController.ForgotPassword)
|
||||||
app.Get("/reset-password", authController.ShowResetPassword)
|
app.Get("/reset-password", authController.ShowResetPassword)
|
||||||
app.Post("/reset-password", authController.ResetPassword)
|
app.Post("/reset-password", authController.ResetPassword)
|
||||||
app.Get("/users", httpmw.RequireAuth(), usersController.Index)
|
app.Get("/welcome", httpmw.RequireAuth(), authController.ShowWelcome)
|
||||||
app.Get("/users/table", httpmw.RequireAuth(), usersController.Table)
|
|
||||||
app.Get("/users/:id/modal", httpmw.RequireAuth(), usersController.Modal)
|
|
||||||
|
|
||||||
private := app.Group("/private", httpmw.RequireAuth())
|
private := app.Group("/private", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||||
private.Get("/", func(c *fiber.Ctx) error {
|
private.Get("/", func(c *fiber.Ctx) error {
|
||||||
return c.Redirect("/users")
|
return c.Redirect("/admin/users")
|
||||||
})
|
})
|
||||||
|
|
||||||
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||||
admin.Get("/", adminController.Dashboard)
|
admin.Get("/", adminController.Dashboard)
|
||||||
admin.Get("/users", adminController.Users)
|
admin.Get("/users", usersController.Index)
|
||||||
|
admin.Get("/users/table", usersController.Table)
|
||||||
|
admin.Get("/users/:id/modal", usersController.Modal)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<nav class="flex items-center justify-between px-6 md:px-16 lg:px-24 xl:px-32 py-4 border-b border-gray-300 bg-white relative transition-all">
|
||||||
|
<a href="https://prebuiltui.com">
|
||||||
|
<img class="h-9" src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/dummyLogo/dummyLogoColored.svg" alt="dummyLogoColored">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="hidden sm:flex items-center gap-8">
|
||||||
|
|
||||||
|
<button class="cursor-pointer px-8 py-2 bg-indigo-500 hover:bg-indigo-600 transition text-white rounded-full">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,21 +4,24 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let title = '';
|
export let title = '';
|
||||||
export let open = false;
|
|
||||||
|
|
||||||
let rootEl: HTMLElement;
|
let rootEl: HTMLElement;
|
||||||
let panelEl: HTMLElement;
|
let panelEl: HTMLElement;
|
||||||
|
let hostEl: HTMLElement;
|
||||||
|
|
||||||
|
function isOpen(): boolean {
|
||||||
|
return !!hostEl?.hasAttribute('open');
|
||||||
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
open = false;
|
hostEl?.removeAttribute('open');
|
||||||
rootEl?.removeAttribute('open');
|
hostEl?.dispatchEvent(
|
||||||
rootEl?.dispatchEvent(
|
|
||||||
new CustomEvent('ui:close', { bubbles: true, composed: true })
|
new CustomEvent('ui:close', { bubbles: true, composed: true })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
if (!open) return;
|
if (!isOpen()) return;
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -64,10 +67,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const host = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
hostEl = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
open = host.hasAttribute('open');
|
if (isOpen()) {
|
||||||
if (open) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const autofocus = panelEl?.querySelector<HTMLElement>('[autofocus]');
|
const autofocus = panelEl?.querySelector<HTMLElement>('[autofocus]');
|
||||||
(autofocus || panelEl)?.focus();
|
(autofocus || panelEl)?.focus();
|
||||||
|
|
@ -75,37 +78,48 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(host, { attributes: true, attributeFilter: ['open'] });
|
observer.observe(hostEl, { attributes: true, attributeFilter: ['open'] });
|
||||||
|
|
||||||
|
if (isOpen()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
(panelEl as HTMLElement | undefined)?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
<div class="overlay" on:click={onBackdropClick} on:keydown={onKeyDown} role="presentation" bind:this={rootEl}>
|
||||||
<div class="overlay" on:click={onBackdropClick} on:keydown={onKeyDown} role="presentation">
|
<div class="panel" role="dialog" aria-modal="true" tabindex="-1" bind:this={panelEl}>
|
||||||
<div class="panel" role="dialog" aria-modal="true" tabindex="-1" bind:this={panelEl}>
|
<header class="header">
|
||||||
<header class="header">
|
<h3>{title}</h3>
|
||||||
<h3>{title}</h3>
|
<button type="button" class="close" on:click={closeModal} aria-label="Close">×</button>
|
||||||
<button type="button" class="close" on:click={closeModal} aria-label="Close">×</button>
|
</header>
|
||||||
</header>
|
<section class="body">
|
||||||
<section class="body">
|
<slot />
|
||||||
<slot />
|
</section>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<div bind:this={rootEl} hidden></div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: var(--ui-overlay);
|
background: var(--ui-overlay);
|
||||||
display: grid;
|
display: none;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([open]) .overlay {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
width: min(640px, calc(100vw - 32px));
|
width: min(640px, calc(100vw - 32px));
|
||||||
max-height: calc(100vh - 48px);
|
max-height: calc(100vh - 48px);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,6 @@
|
||||||
<p class="muted">Area amministrazione.</p>
|
<p class="muted">Area amministrazione.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a href="/admin/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Gestione utenti</a>
|
<a href="/admin/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Gestione utenti</a>
|
||||||
<a href="/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Vista utenti (private)</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
{{define "content"}}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold">Admin - Users</h1>
|
|
||||||
<p class="muted">Elenco utenti server-rendered.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="row items-center" method="get" action="/admin/users">
|
|
||||||
<input class="input-base" type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}">
|
|
||||||
<select class="input-base" name="sort">
|
|
||||||
<option value="id" {{if eq .PageData.Sort "id"}}selected{{end}}>ID</option>
|
|
||||||
<option value="name" {{if eq .PageData.Sort "name"}}selected{{end}}>Name</option>
|
|
||||||
<option value="email" {{if eq .PageData.Sort "email"}}selected{{end}}>Email</option>
|
|
||||||
</select>
|
|
||||||
<select class="input-base" name="dir">
|
|
||||||
<option value="asc" {{if eq .PageData.Dir "asc"}}selected{{end}}>ASC</option>
|
|
||||||
<option value="desc" {{if eq .PageData.Dir "desc"}}selected{{end}}>DESC</option>
|
|
||||||
</select>
|
|
||||||
<input class="input-base w-28" type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}">
|
|
||||||
<input type="hidden" name="page" value="1">
|
|
||||||
<button class="btn-primary" type="submit">Filtra</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b border-slate-200 text-left">
|
|
||||||
<th class="px-2 py-2">ID</th>
|
|
||||||
<th class="px-2 py-2">Name</th>
|
|
||||||
<th class="px-2 py-2">Email</th>
|
|
||||||
<th class="px-2 py-2">Role</th>
|
|
||||||
<th class="px-2 py-2">Verified</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .PageData.Users}}
|
|
||||||
<tr class="border-b border-slate-100">
|
|
||||||
<td class="px-2 py-2">{{.ID}}</td>
|
|
||||||
<td class="px-2 py-2">{{if .Name}}{{.Name}}{{else}}-{{end}}</td>
|
|
||||||
<td class="px-2 py-2">{{.Email}}</td>
|
|
||||||
<td class="px-2 py-2">{{.Role}}</td>
|
|
||||||
<td class="px-2 py-2">{{if .EmailVerified}}yes{{else}}no{{end}}</td>
|
|
||||||
</tr>
|
|
||||||
{{else}}
|
|
||||||
<tr><td colspan="5" class="px-2 py-3">Nessun utente trovato.</td></tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row items-center justify-between">
|
|
||||||
<div class="muted">Totale: {{.PageData.Total}} utenti. Pagina {{.PageData.Page}}{{if gt .PageData.TotalPages 0}} / {{.PageData.TotalPages}}{{end}}</div>
|
|
||||||
<div class="row">
|
|
||||||
<a class="rounded border border-slate-300 px-3 py-1.5 hover:bg-slate-50 {{if not .PageData.HasPrev}}pointer-events-none opacity-50{{end}}" href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.PrevPage}}&pageSize={{.PageData.PageSize}}">Prev</a>
|
|
||||||
<a class="rounded border border-slate-300 px-3 py-1.5 hover:bg-slate-50 {{if not .PageData.HasNext}}pointer-events-none opacity-50{{end}}" href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.NextPage}}&pageSize={{.PageData.PageSize}}">Next</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=id&dir={{if and (eq $p.Sort "id") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">ID</a>
|
<a href="#" hx-get="/admin/users/table?q={{$p.Q}}&sort=id&dir={{if and (eq $p.Sort "id") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">ID</a>
|
||||||
</th>
|
</th>
|
||||||
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=name&dir={{if and (eq $p.Sort "name") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Name</a>
|
<a href="#" hx-get="/admin/users/table?q={{$p.Q}}&sort=name&dir={{if and (eq $p.Sort "name") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Name</a>
|
||||||
</th>
|
</th>
|
||||||
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=email&dir={{if and (eq $p.Sort "email") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Email</a>
|
<a href="#" hx-get="/admin/users/table?q={{$p.Q}}&sort=email&dir={{if and (eq $p.Sort "email") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Email</a>
|
||||||
</th>
|
</th>
|
||||||
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Role</th>
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Role</th>
|
||||||
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Azioni</th>
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Azioni</th>
|
||||||
|
|
@ -25,10 +25,9 @@
|
||||||
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Role}}</td>
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Role}}</td>
|
||||||
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">
|
||||||
<button
|
<button
|
||||||
hx-get="/users/{{$u.ID}}/modal"
|
hx-get="/admin/users/{{$u.ID}}/modal"
|
||||||
hx-target="#userModal"
|
hx-target="#userModalContent"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-on::after-request="document.getElementById('userModal').setAttribute('open','')"
|
|
||||||
>Apri</button>
|
>Apri</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -41,8 +40,8 @@
|
||||||
<div class="row" style="margin-top:12px;align-items:center;justify-content:space-between;">
|
<div class="row" style="margin-top:12px;align-items:center;justify-content:space-between;">
|
||||||
<div class="muted">Totale: {{$p.Total}} utenti. Pagina {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div>
|
<div class="muted">Totale: {{$p.Total}} utenti. Pagina {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button {{if not $p.HasPrev}}disabled{{end}} hx-get="/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.PrevPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Prev</button>
|
<button {{if not $p.HasPrev}}disabled{{end}} hx-get="/admin/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.PrevPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Prev</button>
|
||||||
<button {{if not $p.HasNext}}disabled{{end}} hx-get="/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.NextPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Next</button>
|
<button {{if not $p.HasNext}}disabled{{end}} hx-get="/admin/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.NextPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<h1>Users</h1>
|
||||||
|
<p class="muted">Ricerca, ordinamento e paging server-side via HTMX.</p>
|
||||||
|
|
||||||
|
<form id="usersFilters" class="row" hx-get="/admin/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
|
||||||
|
<input type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}">
|
||||||
|
<input type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" style="max-width:120px;">
|
||||||
|
<input type="hidden" name="sort" value="{{.PageData.Sort}}">
|
||||||
|
<input type="hidden" name="dir" value="{{.PageData.Dir}}">
|
||||||
|
<input type="hidden" name="page" value="1">
|
||||||
|
<button type="submit">Cerca</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="usersTableContainer" hx-get="/admin/users/table?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.Page}}&pageSize={{.PageData.PageSize}}" hx-trigger="load" hx-swap="innerHTML">
|
||||||
|
{{template "users_table" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-modal id="userModal" title="Dettaglio utente">
|
||||||
|
<div
|
||||||
|
id="userModalContent"
|
||||||
|
hx-on:htmx:after-swap="document.getElementById('userModal').setAttribute('open','')"
|
||||||
|
></div>
|
||||||
|
</ui-modal>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var modal = document.getElementById('userModal');
|
||||||
|
var content = document.getElementById('userModalContent');
|
||||||
|
if (!modal || !content || modal.dataset.closeBound === '1') return;
|
||||||
|
modal.dataset.closeBound = '1';
|
||||||
|
modal.addEventListener('ui:close', function () {
|
||||||
|
content.innerHTML = '';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
|
@ -10,17 +10,26 @@
|
||||||
<script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
|
<script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="flex gap-3 bg-slate-900 px-4 py-3 text-white">
|
<nav class="relative flex items-center justify-between border-b border-gray-300 bg-white px-6 py-4 transition-all md:px-16 lg:px-24 xl:px-32">
|
||||||
<a href="/" class="text-slate-200 hover:text-white {{if eq .NavSection "public"}}font-semibold text-white{{end}}">Public</a>
|
<a href="/" class="text-lg font-semibold text-slate-800">Trustcontact</a>
|
||||||
<a href="/private" class="text-slate-200 hover:text-white {{if eq .NavSection "private"}}font-semibold text-white{{end}}">Private</a>
|
|
||||||
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
|
<div class="hidden items-center gap-8 sm:flex">
|
||||||
<a href="/admin" class="text-slate-200 hover:text-white {{if eq .NavSection "admin"}}font-semibold text-white{{end}}">Admin</a>
|
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
|
||||||
{{end}}
|
<a href="/admin" class="text-slate-700 hover:text-slate-900 {{if eq .NavSection "admin"}}font-semibold{{end}}">Admin</a>
|
||||||
{{if .CurrentUser}}
|
{{end}}
|
||||||
<form action="/logout" method="post" class="ml-auto">
|
|
||||||
<button type="submit" class="btn-primary">Logout</button>
|
{{if .CurrentUser}}
|
||||||
</form>
|
<form action="/logout" method="post">
|
||||||
{{end}}
|
<button type="submit" class="cursor-pointer rounded-full bg-indigo-500 px-8 py-2 text-white transition hover:bg-indigo-600">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<a href="/login" class="cursor-pointer rounded-full bg-indigo-500 px-8 py-2 text-white transition hover:bg-indigo-600">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="mx-auto my-5 max-w-5xl px-4">
|
<div class="mx-auto my-5 max-w-5xl px-4">
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{{define "content"}}
|
|
||||||
<h1>Users</h1>
|
|
||||||
<p class="muted">Ricerca, ordinamento e paging server-side via HTMX.</p>
|
|
||||||
|
|
||||||
<form id="usersFilters" class="row" hx-get="/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
|
|
||||||
<input type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}">
|
|
||||||
<input type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" style="max-width:120px;">
|
|
||||||
<input type="hidden" name="sort" value="{{.PageData.Sort}}">
|
|
||||||
<input type="hidden" name="dir" value="{{.PageData.Dir}}">
|
|
||||||
<input type="hidden" name="page" value="1">
|
|
||||||
<button type="submit">Cerca</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="usersTableContainer" hx-get="/users/table?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.Page}}&pageSize={{.PageData.PageSize}}" hx-trigger="load" hx-swap="innerHTML">
|
|
||||||
{{template "users_table" .}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ui-modal id="userModal"></ui-modal>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold">Welcome</h1>
|
||||||
|
{{if .CurrentUser}}
|
||||||
|
<p class="muted">Bentornato {{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}{{.CurrentUser.Email}}{{end}}.</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="muted">Benvenuto.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if and .CurrentUser (ne .CurrentUser.Role "admin")}}
|
||||||
|
<p class="muted">Non hai privilegi admin.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -1,9 +1,25 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Password dimenticata</h1>
|
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
|
||||||
<p class="muted">Inserisci la tua email. Se l'account esiste e risulta verificato, invieremo un link di reset.</p>
|
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-amber-100">
|
||||||
<form action="/forgot-password" method="post">
|
<svg class="h-8 w-8 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<label>Email</label>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 1.657-1.343 3-3 3S6 12.657 6 11s1.343-3 3-3 3 1.343 3 3zm0 0V9a4 4 0 118 0v2m-8 0h8m-8 0H4m16 0v8a2 2 0 01-2 2H6a2 2 0 01-2-2v-8"></path>
|
||||||
<input type="email" name="email" value="{{.Email}}" required>
|
</svg>
|
||||||
<button type="submit">Invia link reset</button>
|
</div>
|
||||||
</form>
|
|
||||||
|
<h3 class="mb-3 text-center text-xl font-bold text-gray-800">Forgot Password</h3>
|
||||||
|
<p class="mb-6 text-center text-sm text-gray-500">Inserisci la tua email. Se l'account esiste e risulta verificato, invieremo un link di reset.</p>
|
||||||
|
|
||||||
|
<form action="/forgot-password" method="post">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
||||||
|
<input type="email" name="email" value="{{.Email}}" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full rounded-lg bg-amber-500 px-4 py-2 font-medium text-white transition duration-300 hover:bg-amber-600">Invia link reset</button>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<a href="/login" class="text-sm text-slate-600 hover:text-slate-800">Torna al login</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Trustcontact</h1>
|
<div class="space-y-3">
|
||||||
<p class="muted">Boilerplate GoFiber + HTMX + Svelte CE + GORM.</p>
|
<h1 class="text-2xl font-semibold">Trustcontact</h1>
|
||||||
<div class="row">
|
<p class="muted">Accedi o registrati per continuare.</p>
|
||||||
<a href="/signup">Crea account</a>
|
<div class="row">
|
||||||
<a href="/login">Accedi</a>
|
<a class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50" href="/login">Accedi</a>
|
||||||
<a href="/forgot-password">Password dimenticata</a>
|
<a class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50" href="/signup">Registrati</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,32 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Login</h1>
|
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
|
||||||
<form action="/login" method="post">
|
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
|
||||||
<label>Email</label>
|
<svg class="h-8 w-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<input type="email" name="email" value="{{.Email}}" required>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
<label>Password</label>
|
</svg>
|
||||||
<input type="password" name="password" required>
|
</div>
|
||||||
<button type="submit">Accedi</button>
|
|
||||||
</form>
|
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Quick Login</h3>
|
||||||
<p class="muted">Non hai un account? <a href="/signup">Registrati</a></p>
|
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Email or Patient ID</label>
|
||||||
|
<input type="text" name="email" value="{{.Email}}" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Password</label>
|
||||||
|
<input type="password" name="password" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full rounded-lg bg-blue-500 px-4 py-2 font-medium text-white transition duration-300 hover:bg-blue-600">Sign In</button>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<a href="/forgot-password" class="text-sm text-blue-500 hover:text-blue-600">Forgot Password?</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<a href="/signup" class="text-sm text-slate-600 hover:text-slate-800">Non hai un account? Registrati</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Reset password</h1>
|
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
|
||||||
{{if .Token}}
|
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-violet-100">
|
||||||
<form action="/reset-password?token={{.Token}}" method="post">
|
<svg class="h-8 w-8 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<label>Nuova password</label>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 11V7a4 4 0 118 0v4m-8 0h8m-8 0H5m14 0v8a2 2 0 01-2 2H7a2 2 0 01-2-2v-8"></path>
|
||||||
<input type="password" name="password" required>
|
</svg>
|
||||||
<button type="submit">Aggiorna password</button>
|
</div>
|
||||||
</form>
|
|
||||||
{{else}}
|
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Reset Password</h3>
|
||||||
<p class="muted">Token mancante o non valido.</p>
|
|
||||||
{{end}}
|
{{if .Token}}
|
||||||
|
<form action="/reset-password?token={{.Token}}" method="post">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Nuova password</label>
|
||||||
|
<input type="password" name="password" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full rounded-lg bg-violet-500 px-4 py-2 font-medium text-white transition duration-300 hover:bg-violet-600">Aggiorna password</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<p class="text-center text-sm text-gray-500">Token mancante o non valido.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Sign up</h1>
|
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
|
||||||
<form action="/signup" method="post">
|
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100">
|
||||||
<label>Email</label>
|
<svg class="h-8 w-8 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<input type="email" name="email" value="{{.Email}}" required>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3M5 7h8M5 11h4m1 10h8a2 2 0 002-2V5a2 2 0 00-2-2H6a2 2 0 00-2 2v14a2 2 0 002 2h4z"></path>
|
||||||
<label>Password</label>
|
</svg>
|
||||||
<input type="password" name="password" required>
|
</div>
|
||||||
<button type="submit">Crea account</button>
|
|
||||||
</form>
|
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Create Account</h3>
|
||||||
<p class="muted">Hai già un account? <a href="/login">Accedi</a></p>
|
|
||||||
|
<form action="/signup" method="post">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
||||||
|
<input type="email" name="email" value="{{.Email}}" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">Password</label>
|
||||||
|
<input type="password" name="password" class="w-full rounded-lg border border-gray-300 px-3 py-2 transition outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full rounded-lg bg-emerald-500 px-4 py-2 font-medium text-white transition duration-300 hover:bg-emerald-600">Sign Up</button>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<a href="/login" class="text-sm text-slate-600 hover:text-slate-800">Hai già un account? Accedi</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue