genarato alcuni partials e corretto drop down

This commit is contained in:
fabio 2026-02-23 17:09:35 +01:00
parent 3fc01cc4f7
commit 2552b8ad8f
35 changed files with 587 additions and 221 deletions

View File

@ -62,7 +62,6 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
- Public: `web/templates/public`
- Private: `web/templates/private`
- Admin: `web/templates/admin`
- Components Flowbite: `web/templates/components`
## Email in Develop

View File

@ -3,3 +3,15 @@
@source "../web/static/**/*.js";
@source "./node_modules/flowbite/**/*.js";
@import "tailwindcss";
@layer utilities {
.flag-lang {
width: 32px;
height: 22px;
}
.flag-lang-ch {
width: 22px;
height: 22px;
}
}

View File

@ -23,6 +23,8 @@ func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
tmpl, err := template.ParseFiles(
"web/templates/layout.html",
"web/templates/admin/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/public/_flash.html",
"web/templates/admin/dashboard.html",
)

View File

@ -19,13 +19,9 @@ func NewAuthController(authService *services.AuthService) *AuthController {
}
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{
"Title": "Home",
"NavSection": "public",
"NavSection": "home",
})
}
@ -145,6 +141,13 @@ func (ac *AuthController) ShowVerifyNotice(c *fiber.Ctx) error {
})
}
func (ac *AuthController) ShowForbidden(c *fiber.Ctx) error {
return renderPublic(c, "forbidden.html", map[string]any{
"Title": "Forbidden",
"NavSection": "public",
})
}
func (ac *AuthController) ShowForgotPassword(c *fiber.Ctx) error {
return renderPublic(c, "forgot_password.html", map[string]any{
"Title": "Forgot password",

View File

@ -26,6 +26,8 @@ func renderPublic(c *fiber.Ctx, page string, data map[string]any) error {
files := []string{
"web/templates/layout.html",
"web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/public/_flash.html",
filepath.Join("web/templates/public", page),
}
@ -62,6 +64,8 @@ func renderPrivate(c *fiber.Ctx, page string, data map[string]any) error {
files := []string{
"web/templates/layout.html",
"web/templates/private/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/public/_flash.html",
filepath.Join("web/templates/private", page),
}

View File

@ -38,6 +38,8 @@ func (uc *UsersController) Index(c *fiber.Ctx) error {
tmpl, err := template.ParseFiles(
"web/templates/layout.html",
"web/templates/admin/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/public/_flash.html",
"web/templates/admin/users/index.html",
"web/templates/admin/users/_table.html",

View File

@ -25,7 +25,7 @@ func RequireAdmin() fiber.Handler {
return c.Redirect("/login")
}
if user.Role != models.RoleAdmin {
return c.Status(fiber.StatusForbidden).SendString("forbidden")
return c.Status(fiber.StatusForbidden).Redirect("/forbidden")
}
return c.Next()
}

View File

@ -48,6 +48,7 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
app.Post("/forgot-password", authController.ForgotPassword)
app.Get("/reset-password", authController.ShowResetPassword)
app.Post("/reset-password", authController.ResetPassword)
app.Get("/forbidden", authController.ShowForbidden)
app.Get("/welcome", httpmw.RequireAuth(), authController.ShowWelcome)
private := app.Group("/private", httpmw.RequireAuth(), httpmw.RequireAdmin())

View File

@ -901,6 +901,9 @@
.mx-auto {
margin-inline: auto;
}
.my-2 {
margin-block: calc(var(--spacing) * 2);
}
.my-4 {
margin-block: calc(var(--spacing) * 4);
}
@ -1460,6 +1463,9 @@
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-44 {
width: calc(var(--spacing) * 44);
}
@ -1549,6 +1555,9 @@
border-color: var(--color-blue-600);
}
}
.min-w-\[95px\] {
min-width: 95px;
}
.flex-1 {
flex: 1;
}
@ -1734,6 +1743,9 @@
.overflow-y-auto {
overflow-y: auto;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
@ -2083,6 +2095,9 @@
stroke: var(--color-gray-700) !important;
}
}
.object-cover {
object-fit: cover;
}
.apexcharts-legend {
.apexcharts-canvas & {
padding: 0 !important;
@ -2138,6 +2153,9 @@
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
@ -2274,6 +2292,10 @@
--tw-leading: calc(var(--spacing) * 9);
line-height: calc(var(--spacing) * 9);
}
.leading-none {
--tw-leading: 1;
line-height: 1;
}
.leading-relaxed {
--tw-leading: var(--leading-relaxed);
line-height: var(--leading-relaxed);
@ -2600,11 +2622,6 @@
margin-inline-start: calc(var(--spacing) * 4);
}
}
.md\:me-0 {
@media (width >= 48rem) {
margin-inline-end: calc(var(--spacing) * 0);
}
}
.md\:mt-0 {
@media (width >= 48rem) {
margin-top: calc(var(--spacing) * 0);
@ -2900,6 +2917,16 @@
}
}
}
@layer utilities {
.flag-lang {
width: 32px;
height: 22px;
}
.flag-lang-ch {
width: 22px;
height: 22px;
}
}
@layer base {
.tooltip-arrow,.tooltip-arrow:before {
position: absolute;

5
web/static/vendor/flags/ch.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<rect width="24" height="24" fill="#d52b1e"/>
<rect x="10" y="5" width="4" height="14" fill="#ffffff"/>
<rect x="5" y="10" width="14" height="4" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

5
web/static/vendor/flags/de.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" aria-hidden="true">
<rect width="3" height="0.6667" x="0" y="0" fill="#000000"/>
<rect width="3" height="0.6667" x="0" y="0.6667" fill="#dd0000"/>
<rect width="3" height="0.6667" x="0" y="1.3333" fill="#ffce00"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

11
web/static/vendor/flags/en.svg vendored Normal file
View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" aria-hidden="true">
<clipPath id="s"><path d="M0,0 v30 h60 v-30 z"/></clipPath>
<clipPath id="t"><path d="M30,15 h30 v15 z v-30 h-30 z h-30 v15 z v15 h30 z"/></clipPath>
<g clip-path="url(#s)">
<path d="M0,0 v30 h60 v-30 z" fill="#012169"/>
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/>
<path d="M0,0 L60,30 M60,0 L0,30" clip-path="url(#t)" stroke="#C8102E" stroke-width="4"/>
<path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/>
<path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 628 B

12
web/static/vendor/flags/en_us.svg vendored Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190 100" aria-hidden="true">
<rect width="190" height="100" fill="#b22234"/>
<g fill="#fff">
<rect y="7.69" width="190" height="7.69"/>
<rect y="23.08" width="190" height="7.69"/>
<rect y="38.46" width="190" height="7.69"/>
<rect y="53.85" width="190" height="7.69"/>
<rect y="69.23" width="190" height="7.69"/>
<rect y="84.62" width="190" height="7.69"/>
</g>
<rect width="76" height="53.85" fill="#3c3b6e"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

5
web/static/vendor/flags/fr.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" aria-hidden="true">
<rect width="1" height="2" x="0" y="0" fill="#0055a4"/>
<rect width="1" height="2" x="1" y="0" fill="#ffffff"/>
<rect width="1" height="2" x="2" y="0" fill="#ef4135"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

5
web/static/vendor/flags/it.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" aria-hidden="true">
<rect width="1" height="2" x="0" y="0" fill="#009246"/>
<rect width="1" height="2" x="1" y="0" fill="#ffffff"/>
<rect width="1" height="2" x="2" y="0" fill="#ce2b37"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@ -0,0 +1,53 @@
{{define "navbar"}}
<nav class="border-b border-gray-200 bg-white">
<div class="mx-auto flex max-w-7xl flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a>
<button data-collapse-toggle="navbar-main" type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 md:hidden" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only" data-i18n="nav.open_main_menu">Apri menu principale</span>
<svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto" id="navbar-main">
<ul class="mt-4 flex flex-col gap-2 rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium md:mt-0 md:flex-row md:items-center md:gap-1 md:border-0 md:bg-transparent md:p-0">
<li><a href="/admin" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100">Dashboard</a></li>
<li><a href="/admin/users" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100" data-i18n="nav.users">Users</a></li>
</ul>
<div class="mt-4 flex items-center gap-3 md:mt-0 md:ms-4">
{{template "navbar_controls" .}}
</div>
</div>
</div>
</nav>
{{end}}
{{define "navbar_controls"}}
{{template "language_dropdown" .}}
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm" id="user-dropdown">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500">{{.CurrentUser.Email}}</span>
</div>
<div class="py-2">
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50" data-i18n="nav.logout">Logout</button>
</form>
</div>
</div>
</div>
{{end}}
{{end}}

View File

@ -1,14 +1,14 @@
{{define "content"}}
<section class="space-y-6">
<h1 class="text-3xl font-bold text-gray-900">Audit Logs</h1>
<h1 class="text-3xl font-bold text-gray-900" data-i18n="audit.title">Audit Logs</h1>
<div class="mb-4 border-b border-gray-200">
<ul class="-mb-px flex flex-wrap text-center text-sm font-medium" id="audit-tab" data-tabs-toggle="#audit-tab-content" role="tablist">
<li class="me-2" role="presentation">
<button class="inline-block rounded-t-lg border-b-2 p-4" id="activity-tab" data-tabs-target="#activity" type="button" role="tab" aria-controls="activity" aria-selected="true">Activity</button>
<button class="inline-block rounded-t-lg border-b-2 p-4" id="activity-tab" data-tabs-target="#activity" type="button" role="tab" aria-controls="activity" aria-selected="true" data-i18n="audit.activity">Activity</button>
</li>
<li class="me-2" role="presentation">
<button class="inline-block rounded-t-lg border-b-2 p-4" id="security-tab" data-tabs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="false">Security</button>
<button class="inline-block rounded-t-lg border-b-2 p-4" id="security-tab" data-tabs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="false" data-i18n="audit.security">Security</button>
</li>
</ul>
</div>
@ -18,14 +18,18 @@
<div class="relative overflow-x-auto">
<table class="w-full text-left text-sm text-gray-500">
<thead class="bg-gray-50 text-xs uppercase text-gray-700">
<tr><th class="px-6 py-3">Timestamp</th><th class="px-6 py-3">Actor</th><th class="px-6 py-3">Action</th></tr>
<tr>
<th class="px-6 py-3" data-i18n="audit.timestamp">Timestamp</th>
<th class="px-6 py-3" data-i18n="audit.actor">Actor</th>
<th class="px-6 py-3" data-i18n="audit.action">Action</th>
</tr>
</thead>
<tbody><tr class="border-b bg-white"><td class="px-6 py-4">-</td><td class="px-6 py-4">-</td><td class="px-6 py-4">-</td></tr></tbody>
</table>
</div>
</div>
<div class="hidden rounded-lg border border-gray-200 bg-white p-4 shadow-sm" id="security" role="tabpanel" aria-labelledby="security-tab">
<p class="text-sm text-gray-600">Security logs placeholder.</p>
<p class="text-sm text-gray-600" data-i18n="audit.placeholder">Security logs placeholder.</p>
</div>
</div>
</section>

View File

@ -1,28 +1,28 @@
{{define "content"}}
<section class="space-y-6">
<div>
<h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<p class="text-gray-600">Area amministrazione.</p>
<h1 class="text-3xl font-bold text-gray-900" data-i18n="admin.dashboard.title">Admin Dashboard</h1>
<p class="text-gray-600" data-i18n="admin.dashboard.area">Area amministrazione.</p>
</div>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium text-gray-500">Utenti</p>
<p class="text-sm font-medium text-gray-500" data-i18n="admin.users_count">Utenti</p>
<p class="mt-2 text-3xl font-semibold text-gray-900">{{if .PageData}}{{.PageData.Total}}{{else}}-{{end}}</p>
</article>
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium text-gray-500">Ruolo corrente</p>
<p class="text-sm font-medium text-gray-500" data-i18n="admin.current_role">Ruolo corrente</p>
<p class="mt-2">
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">admin</span>
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800" data-i18n="user.role_admin">admin</span>
{{else}}
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">user</span>
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800" data-i18n="user.role_user">user</span>
{{end}}
</p>
</article>
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium text-gray-500">Navigazione</p>
<a href="/admin/users" class="mt-2 inline-flex rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white hover:bg-blue-800">Gestione utenti</a>
<p class="text-sm font-medium text-gray-500" data-i18n="admin.navigation">Navigazione</p>
<a href="/admin/users" class="mt-2 inline-flex rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white hover:bg-blue-800" data-i18n="admin.manage_users">Gestione utenti</a>
</article>
</div>
</section>

View File

@ -1,8 +1,8 @@
{{define "content"}}
<section class="space-y-4">
<h1 class="text-3xl font-bold text-gray-900">Admin Users</h1>
<h1 class="text-3xl font-bold text-gray-900" data-i18n="users.title">Admin Users</h1>
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<p class="text-gray-600">Template pagina utenti admin in stile Flowbite.</p>
<p class="text-gray-600" data-i18n="users.new_user_modal_placeholder">Template pagina utenti admin in stile Flowbite.</p>
</div>
</section>
{{end}}

View File

@ -1,36 +1,36 @@
{{define "users_modal"}}
<div class="grid gap-3 text-sm text-gray-700 sm:grid-cols-2">
<div>
<span class="font-semibold text-gray-900">ID:</span>
<span class="font-semibold text-gray-900" data-i18n="table.id">ID</span>:
<span>{{.User.ID}}</span>
</div>
<div>
<span class="font-semibold text-gray-900">Role:</span>
<span class="font-semibold text-gray-900" data-i18n="table.role">Role</span>:
{{if eq .User.Role "admin"}}
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">admin</span>
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800" data-i18n="user.role_admin">admin</span>
{{else}}
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">user</span>
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800" data-i18n="user.role_user">user</span>
{{end}}
</div>
<div class="sm:col-span-2">
<span class="font-semibold text-gray-900">Name:</span>
<span class="font-semibold text-gray-900" data-i18n="table.name">Name</span>:
<span>{{if .User.Name}}{{.User.Name}}{{else}}-{{end}}</span>
</div>
<div class="sm:col-span-2">
<span class="font-semibold text-gray-900">Email:</span>
<span class="font-semibold text-gray-900" data-i18n="table.email">Email</span>:
<span>{{.User.Email}}</span>
</div>
<div>
<span class="font-semibold text-gray-900">Verified:</span>
<span>{{if .User.EmailVerified}}yes{{else}}no{{end}}</span>
<span class="font-semibold text-gray-900" data-i18n="user.verified">Verified</span>:
<span>{{if .User.EmailVerified}}<span data-i18n="user.yes">yes</span>{{else}}<span data-i18n="user.no">no</span>{{end}}</span>
</div>
<div>
<span class="font-semibold text-gray-900">Created:</span>
<span>{{.User.CreatedAt}}</span>
<span class="font-semibold text-gray-900" data-i18n="user.created">Created</span>:
<span data-localize-date="{{.User.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}">{{.User.CreatedAt}}</span>
</div>
</div>
<div class="mt-5">
<button type="button" class="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800" data-modal-hide="userModal">Chiudi</button>
<button type="button" class="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800" data-modal-hide="userModal" data-i18n="users.close">Chiudi</button>
</div>
{{end}}

View File

@ -5,16 +5,16 @@
<thead class="bg-gray-50 text-xs uppercase text-gray-700">
<tr>
<th scope="col" class="px-6 py-3">
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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</button>
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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" data-i18n="table.id">ID</button>
</th>
<th scope="col" class="px-6 py-3">
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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</button>
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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" data-i18n="table.name">Name</button>
</th>
<th scope="col" class="px-6 py-3">
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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</button>
<button type="button" class="inline-flex items-center gap-1 hover:text-blue-700" 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" data-i18n="table.email">Email</button>
</th>
<th scope="col" class="px-6 py-3">Role</th>
<th scope="col" class="px-6 py-3">Azioni</th>
<th scope="col" class="px-6 py-3" data-i18n="table.role">Role</th>
<th scope="col" class="px-6 py-3" data-i18n="users.actions">Azioni</th>
</tr>
</thead>
<tbody>
@ -25,18 +25,18 @@
<td class="px-6 py-4">{{$u.Email}}</td>
<td class="px-6 py-4">
{{if eq $u.Role "admin"}}
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">admin</span>
<span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800" data-i18n="user.role_admin">admin</span>
{{else}}
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">user</span>
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800" data-i18n="user.role_user">user</span>
{{end}}
</td>
<td class="px-6 py-4">
<button type="button" class="rounded-lg bg-blue-700 px-3 py-2 text-xs font-medium text-white hover:bg-blue-800" data-modal-target="userModal" data-modal-toggle="userModal" hx-get="/admin/users/{{$u.ID}}/modal" hx-target="#userModalContent" hx-swap="innerHTML">Apri</button>
<button type="button" class="rounded-lg bg-blue-700 px-3 py-2 text-xs font-medium text-white hover:bg-blue-800" data-modal-target="userModal" data-modal-toggle="userModal" hx-get="/admin/users/{{$u.ID}}/modal" hx-target="#userModalContent" hx-swap="innerHTML" data-i18n="users.open">Apri</button>
</td>
</tr>
{{else}}
<tr class="bg-white">
<td colspan="5" class="px-6 py-4 text-center text-gray-500">Nessun utente trovato.</td>
<td colspan="5" class="px-6 py-4 text-center text-gray-500" data-i18n="users.none">Nessun utente trovato.</td>
</tr>
{{end}}
</tbody>
@ -44,10 +44,10 @@
</div>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
<div class="text-sm text-gray-600">Totale: {{$p.Total}} utenti. Pagina {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div>
<div class="text-sm text-gray-600"><span data-i18n="users.total">Totale</span>: {{$p.Total}} <span data-i18n="users.users_label">utenti</span>. <span data-i18n="users.page">Pagina</span> {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div>
<div class="inline-flex gap-2">
<button type="button" class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" {{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 type="button" class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" {{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>
<button type="button" class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" {{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" data-i18n="users.prev">Prev</button>
<button type="button" class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" {{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" data-i18n="users.next">Next</button>
</div>
</div>
{{end}}

View File

@ -2,25 +2,25 @@
<section class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-3xl font-bold text-gray-900">Users</h1>
<p class="text-gray-600">Ricerca, ordinamento e paging server-side via HTMX.</p>
<h1 class="text-3xl font-bold text-gray-900" data-i18n="users.title">Users</h1>
<p class="text-gray-600" data-i18n="users.subtitle">Ricerca, ordinamento e paging server-side via HTMX.</p>
</div>
<button type="button" data-modal-target="newUserModal" data-modal-toggle="newUserModal" class="inline-flex items-center rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">
<button type="button" data-modal-target="newUserModal" data-modal-toggle="newUserModal" class="inline-flex items-center rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="users.new_user">
Nuovo Utente
</button>
</div>
<form id="usersFilters" class="grid gap-3 rounded-lg border border-gray-200 bg-white p-4 shadow-sm md:grid-cols-4" hx-get="/admin/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
<div class="md:col-span-2">
<label for="users-q" class="mb-2 block text-sm font-medium text-gray-900">Search</label>
<input id="users-q" type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500">
<label for="users-q" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="users.search">Search</label>
<input id="users-q" type="text" name="q" placeholder="Cerca nome o email" data-i18n-placeholder="users.search_placeholder" value="{{.PageData.Q}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500">
</div>
<div>
<label for="users-size" class="mb-2 block text-sm font-medium text-gray-900">Page size</label>
<label for="users-size" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="users.page_size">Page size</label>
<input id="users-size" type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500">
</div>
<div class="flex items-end">
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">Cerca</button>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="users.search_button">Cerca</button>
</div>
<input type="hidden" name="sort" value="{{.PageData.Sort}}">
<input type="hidden" name="dir" value="{{.PageData.Dir}}">
@ -36,9 +36,9 @@
<div class="relative max-h-full w-full max-w-2xl">
<div class="relative rounded-lg bg-white shadow-sm">
<div class="flex items-start justify-between rounded-t border-b p-4 md:p-5">
<h3 class="text-xl font-semibold text-gray-900">Dettaglio utente</h3>
<h3 class="text-xl font-semibold text-gray-900" data-i18n="users.user_detail">Dettaglio utente</h3>
<button type="button" class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900" data-modal-hide="userModal" aria-label="Close modal">
<span class="sr-only">Close modal</span>
<span class="sr-only" data-i18n="users.close">Close modal</span>
<svg class="h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
@ -53,18 +53,18 @@
<div class="relative max-h-full w-full max-w-xl">
<div class="relative rounded-lg bg-white shadow-sm">
<div class="flex items-start justify-between rounded-t border-b p-4 md:p-5">
<h3 class="text-xl font-semibold text-gray-900">Nuovo utente</h3>
<h3 class="text-xl font-semibold text-gray-900" data-i18n="users.new_user_modal_title">Nuovo utente</h3>
<button type="button" class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900" data-modal-hide="newUserModal" aria-label="Close modal">
<span class="sr-only">Close modal</span>
<span class="sr-only" data-i18n="users.close">Close modal</span>
<svg class="h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
</button>
</div>
<div class="space-y-4 p-4 md:p-5">
<p class="text-sm text-gray-600">Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.</p>
<p class="text-sm text-gray-600" data-i18n="users.new_user_modal_placeholder">Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.</p>
<div>
<label for="new-user-email" class="mb-2 block text-sm font-medium text-gray-900">Email</label>
<label for="new-user-email" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.email">Email</label>
<input id="new-user-email" type="email" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900" placeholder="name@company.com" disabled>
</div>
</div>

View File

@ -8,82 +8,209 @@
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/flowbite.js"></script>
</head>
<body class="bg-gray-50 text-gray-900 antialiased">
<nav class="border-b border-gray-200 bg-white">
<div class="mx-auto flex max-w-7xl flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a>
<body class="flex min-h-screen flex-col bg-gray-50 text-gray-900 antialiased">
{{template "navbar" .}}
<button data-collapse-toggle="navbar-main" type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 md:hidden" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only">Apri menu principale</span>
<svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto" id="navbar-main">
<ul class="mt-4 flex flex-col gap-2 rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium md:mt-0 md:flex-row md:items-center md:gap-1 md:border-0 md:bg-transparent md:p-0">
{{if .CurrentUser}}
<li>
<a href="/welcome" class="block rounded-lg px-3 py-2 {{if eq .NavSection "private"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}">Dashboard</a>
</li>
<li>
<a href="/admin/users" class="block rounded-lg px-3 py-2 {{if eq .NavSection "users"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}">Users</a>
</li>
{{if eq .CurrentUser.Role "admin"}}
<li>
<a href="/admin" class="block rounded-lg px-3 py-2 {{if eq .NavSection "admin"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}">Admin</a>
</li>
{{end}}
{{else}}
<li>
<a href="/login" class="block rounded-lg px-3 py-2 {{if eq .NavSection "login"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}">Login</a>
</li>
<li>
<a href="/signup" class="block rounded-lg px-3 py-2 {{if eq .NavSection "signup"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}">Signup</a>
</li>
{{end}}
</ul>
{{if .CurrentUser}}
<div class="relative mt-4 md:mt-0 md:ms-4">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300 md:me-0" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm" id="user-dropdown">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500">{{.CurrentUser.Email}}</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li><a href="/welcome" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a></li>
<li><a href="/admin/users" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Users</a></li>
</ul>
<div class="py-2">
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50">Logout</button>
</form>
</div>
</div>
</div>
{{end}}
</div>
</div>
</nav>
<main class="mx-auto max-w-7xl p-6">
<main class="mx-auto w-full max-w-7xl flex-1 p-6">
{{template "_flash.html" .}}
{{template "content" .}}
</main>
<footer class="border-t border-gray-200 bg-white">
<div class="mx-auto max-w-7xl px-6 py-4 text-sm text-gray-500">
Trustcontact
</div>
<div class="mx-auto max-w-7xl px-6 py-4 text-sm text-gray-500">Trustcontact</div>
</footer>
<script>
(function () {
var DEFAULT_LANG = 'it';
var STORAGE_KEY = 'tc_lang';
var dictionaries = {
it: {
'nav.open_main_menu': 'Apri menu principale', 'nav.open_user_menu': 'Apri menu utente', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
'home.subtitle': 'Accedi o registrati per continuare.', 'home.login': 'Accedi', 'home.signup': 'Registrati',
'login.title': 'Login', 'login.subtitle': 'Accedi al tuo account.', 'form.email': 'Email', 'form.password': 'Password', 'login.submit': 'Entra', 'login.forgot': 'Password dimenticata?', 'login.create_account': 'Crea account',
'signup.title': 'Registrazione', 'signup.subtitle': 'Crea il tuo account.', 'signup.submit': 'Registrati', 'signup.has_account': 'Hai già un account?', 'signup.login': 'Accedi',
'forgot.title': 'Password dimenticata', 'forgot.subtitle': 'Inserisci la tua email per ricevere il link di reset.', 'forgot.submit': 'Invia link reset', 'forgot.back_login': 'Torna al login',
'reset.title': 'Reset password', 'reset.subtitle': 'Imposta una nuova password.', 'reset.new_password': 'Nuova password', 'reset.submit': 'Aggiorna password', 'reset.invalid_token': 'Token mancante o non valido.',
'verify.title': 'Verifica email', 'verify.p1': 'Controlla la casella di posta e apri il link di verifica ricevuto.', 'verify.p2': 'Se il link è scaduto, ripeti la registrazione o contatta supporto.', 'verify.go_login': 'Vai al login',
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Bentornato', 'welcome.generic': 'Benvenuto.', 'welcome.quick_links': 'Link rapidi',
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Area amministrazione.', 'admin.users_count': 'Utenti', 'admin.current_role': 'Ruolo corrente', 'admin.navigation': 'Navigazione', 'admin.manage_users': 'Gestione utenti',
'users.title': 'Users', 'users.subtitle': 'Ricerca, ordinamento e paging server-side via HTMX.', 'users.new_user': 'Nuovo Utente', 'users.search': 'Search', 'users.search_placeholder': 'Cerca nome o email', 'users.page_size': 'Page size', 'users.search_button': 'Cerca', 'users.user_detail': 'Dettaglio utente', 'users.actions': 'Azioni', 'users.open': 'Apri', 'users.none': 'Nessun utente trovato.', 'users.total': 'Totale', 'users.users_label': 'utenti', 'users.page': 'Pagina', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Chiudi',
'users.new_user_modal_title': 'Nuovo utente', 'users.new_user_modal_placeholder': 'Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verificato', 'user.created': 'Creato', 'user.yes': 'sì', 'user.no': 'no',
'audit.title': 'Audit Logs', 'audit.activity': 'Attività', 'audit.security': 'Sicurezza', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Attore', 'audit.action': 'Azione', 'audit.placeholder': 'Placeholder log di sicurezza.'
},
en: {
'nav.open_main_menu': 'Open main menu', 'nav.open_user_menu': 'Open user menu', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
'home.subtitle': 'Login or sign up to continue.', 'home.login': 'Login', 'home.signup': 'Sign up',
'login.title': 'Login', 'login.subtitle': 'Access your account.', 'form.email': 'Email', 'form.password': 'Password', 'login.submit': 'Sign in', 'login.forgot': 'Forgot password?', 'login.create_account': 'Create account',
'signup.title': 'Signup', 'signup.subtitle': 'Create your account.', 'signup.submit': 'Sign up', 'signup.has_account': 'Already have an account?', 'signup.login': 'Login',
'forgot.title': 'Forgot Password', 'forgot.subtitle': 'Enter your email to receive a reset link.', 'forgot.submit': 'Send reset link', 'forgot.back_login': 'Back to login',
'reset.title': 'Reset Password', 'reset.subtitle': 'Set a new password.', 'reset.new_password': 'New password', 'reset.submit': 'Update password', 'reset.invalid_token': 'Missing or invalid token.',
'verify.title': 'Verify email', 'verify.p1': 'Check your inbox and open the verification link.', 'verify.p2': 'If the link expired, sign up again or contact support.', 'verify.go_login': 'Go to login',
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Welcome back', 'welcome.generic': 'Welcome.', 'welcome.quick_links': 'Quick links',
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Administration area.', 'admin.users_count': 'Users', 'admin.current_role': 'Current role', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Manage users',
'users.title': 'Users', 'users.subtitle': 'Search, sorting and server-side paging via HTMX.', 'users.new_user': 'New user', 'users.search': 'Search', 'users.search_placeholder': 'Search by name or email', 'users.page_size': 'Page size', 'users.search_button': 'Search', 'users.user_detail': 'User details', 'users.actions': 'Actions', 'users.open': 'Open', 'users.none': 'No users found.', 'users.total': 'Total', 'users.users_label': 'users', 'users.page': 'Page', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Close',
'users.new_user_modal_title': 'New user', 'users.new_user_modal_placeholder': 'Flowbite placeholder UI. Connect creation to backend route when available.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verified', 'user.created': 'Created', 'user.yes': 'yes', 'user.no': 'no',
'audit.title': 'Audit Logs', 'audit.activity': 'Activity', 'audit.security': 'Security', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Actor', 'audit.action': 'Action', 'audit.placeholder': 'Security logs placeholder.'
},
en_us: {},
de: {
'nav.open_main_menu': 'Hauptmenü öffnen', 'nav.open_user_menu': 'Benutzermenü öffnen', 'nav.dashboard': 'Dashboard', 'nav.users': 'Benutzer', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Registrieren', 'nav.logout': 'Abmelden',
'home.subtitle': 'Melden Sie sich an oder registrieren Sie sich, um fortzufahren.', 'home.login': 'Anmelden', 'home.signup': 'Registrieren', 'login.title': 'Login', 'login.subtitle': 'Melden Sie sich in Ihrem Konto an.', 'form.email': 'E-Mail', 'form.password': 'Passwort', 'login.submit': 'Anmelden', 'login.forgot': 'Passwort vergessen?', 'login.create_account': 'Konto erstellen',
'signup.title': 'Registrierung', 'signup.subtitle': 'Erstellen Sie Ihr Konto.', 'signup.submit': 'Registrieren', 'signup.has_account': 'Haben Sie bereits ein Konto?', 'signup.login': 'Anmelden',
'forgot.title': 'Passwort vergessen', 'forgot.subtitle': 'Geben Sie Ihre E-Mail ein, um einen Reset-Link zu erhalten.', 'forgot.submit': 'Reset-Link senden', 'forgot.back_login': 'Zurück zum Login',
'reset.title': 'Passwort zurücksetzen', 'reset.subtitle': 'Legen Sie ein neues Passwort fest.', 'reset.new_password': 'Neues Passwort', 'reset.submit': 'Passwort aktualisieren', 'reset.invalid_token': 'Token fehlt oder ist ungültig.',
'verify.title': 'E-Mail verifizieren', 'verify.p1': 'Öffnen Sie die Verifizierungs-E-Mail in Ihrem Posteingang.', 'verify.p2': 'Wenn der Link abgelaufen ist, registrieren Sie sich erneut oder kontaktieren Sie den Support.', 'verify.go_login': 'Zum Login',
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Willkommen zurück', 'welcome.generic': 'Willkommen.', 'welcome.quick_links': 'Schnelllinks',
'admin.dashboard.title': 'Admin-Dashboard', 'admin.dashboard.area': 'Administrationsbereich.', 'admin.users_count': 'Benutzer', 'admin.current_role': 'Aktuelle Rolle', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Benutzer verwalten',
'users.title': 'Benutzer', 'users.subtitle': 'Suche, Sortierung und serverseitiges Paging via HTMX.', 'users.new_user': 'Neuer Benutzer', 'users.search': 'Suche', 'users.search_placeholder': 'Nach Name oder E-Mail suchen', 'users.page_size': 'Seitengröße', 'users.search_button': 'Suchen', 'users.user_detail': 'Benutzerdetails', 'users.actions': 'Aktionen', 'users.open': 'Öffnen', 'users.none': 'Keine Benutzer gefunden.', 'users.total': 'Gesamt', 'users.users_label': 'Benutzer', 'users.page': 'Seite', 'users.prev': 'Zurück', 'users.next': 'Weiter', 'users.close': 'Schließen',
'users.new_user_modal_title': 'Neuer Benutzer', 'users.new_user_modal_placeholder': 'Flowbite-Placeholder-UI. Bei Bedarf mit Backend-Route verbinden.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'E-Mail', 'table.role': 'Rolle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verifiziert', 'user.created': 'Erstellt', 'user.yes': 'ja', 'user.no': 'nein',
'audit.title': 'Audit-Logs', 'audit.activity': 'Aktivität', 'audit.security': 'Sicherheit', 'audit.timestamp': 'Zeitstempel', 'audit.actor': 'Akteur', 'audit.action': 'Aktion', 'audit.placeholder': 'Platzhalter für Sicherheitsprotokolle.'
},
fr: {
'nav.open_main_menu': 'Ouvrir le menu principal', 'nav.open_user_menu': 'Ouvrir le menu utilisateur', 'nav.dashboard': 'Tableau de bord', 'nav.users': 'Utilisateurs', 'nav.admin': 'Admin', 'nav.login': 'Connexion', 'nav.signup': 'Inscription', 'nav.logout': 'Déconnexion',
'home.subtitle': 'Connectez-vous ou inscrivez-vous pour continuer.', 'home.login': 'Connexion', 'home.signup': 'Inscription', 'login.title': 'Connexion', 'login.subtitle': 'Accédez à votre compte.', 'form.email': 'Email', 'form.password': 'Mot de passe', 'login.submit': 'Se connecter', 'login.forgot': 'Mot de passe oublié ?', 'login.create_account': 'Créer un compte',
'signup.title': 'Inscription', 'signup.subtitle': 'Créez votre compte.', 'signup.submit': 'Sinscrire', 'signup.has_account': 'Vous avez déjà un compte ?', 'signup.login': 'Connexion',
'forgot.title': 'Mot de passe oublié', 'forgot.subtitle': 'Entrez votre email pour recevoir un lien de réinitialisation.', 'forgot.submit': 'Envoyer le lien', 'forgot.back_login': 'Retour à la connexion',
'reset.title': 'Réinitialiser le mot de passe', 'reset.subtitle': 'Définissez un nouveau mot de passe.', 'reset.new_password': 'Nouveau mot de passe', 'reset.submit': 'Mettre à jour', 'reset.invalid_token': 'Jeton manquant ou invalide.',
'verify.title': 'Vérifier lemail', 'verify.p1': 'Vérifiez votre boîte mail et ouvrez le lien de vérification.', 'verify.p2': 'Si le lien a expiré, réinscrivez-vous ou contactez le support.', 'verify.go_login': 'Aller à la connexion',
'welcome.title': 'Tableau de bord', 'welcome.back_prefix': 'Bon retour', 'welcome.generic': 'Bienvenue.', 'welcome.quick_links': 'Liens rapides',
'admin.dashboard.title': 'Tableau de bord admin', 'admin.dashboard.area': 'Zone dadministration.', 'admin.users_count': 'Utilisateurs', 'admin.current_role': 'Rôle actuel', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Gérer les utilisateurs',
'users.title': 'Utilisateurs', 'users.subtitle': 'Recherche, tri et pagination côté serveur via HTMX.', 'users.new_user': 'Nouvel utilisateur', 'users.search': 'Recherche', 'users.search_placeholder': 'Rechercher par nom ou email', 'users.page_size': 'Taille de page', 'users.search_button': 'Rechercher', 'users.user_detail': 'Détails utilisateur', 'users.actions': 'Actions', 'users.open': 'Ouvrir', 'users.none': 'Aucun utilisateur trouvé.', 'users.total': 'Total', 'users.users_label': 'utilisateurs', 'users.page': 'Page', 'users.prev': 'Préc.', 'users.next': 'Suiv.', 'users.close': 'Fermer',
'users.new_user_modal_title': 'Nouvel utilisateur', 'users.new_user_modal_placeholder': 'UI Flowbite placeholder. Connecter à une route backend si nécessaire.',
'table.id': 'ID', 'table.name': 'Nom', 'table.email': 'Email', 'table.role': 'Rôle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Vérifié', 'user.created': 'Créé', 'user.yes': 'oui', 'user.no': 'non',
'audit.title': 'Journaux daudit', 'audit.activity': 'Activité', 'audit.security': 'Sécurité', 'audit.timestamp': 'Horodatage', 'audit.actor': 'Acteur', 'audit.action': 'Action', 'audit.placeholder': 'Espace réservé des journaux de sécurité.'
}
};
dictionaries.en_us = Object.assign({}, dictionaries.en);
dictionaries.de_ch = Object.assign({}, dictionaries.de);
dictionaries.fr_ch = Object.assign({}, dictionaries.fr);
function getLang() {
var stored = localStorage.getItem(STORAGE_KEY);
return dictionaries[stored] ? stored : DEFAULT_LANG;
}
function t(key, lang) {
if (dictionaries[lang] && dictionaries[lang][key]) return dictionaries[lang][key];
if (dictionaries.it[key]) return dictionaries.it[key];
return key;
}
function localeFromLang(lang) {
var localeMap = {
it: 'it-IT',
en: 'en-GB',
en_us: 'en-US',
de: 'de-DE',
fr: 'fr-FR',
de_ch: 'de-CH',
fr_ch: 'fr-CH'
};
return localeMap[lang] || 'it-IT';
}
function localizeDate(rawValue, lang) {
if (!rawValue) return '';
var date = new Date(rawValue);
if (isNaN(date.getTime())) return rawValue;
return new Intl.DateTimeFormat(localeFromLang(lang), {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
}
function applyTranslations(root) {
var lang = getLang();
document.documentElement.setAttribute('lang', lang.replace('_', '-'));
var label = document.getElementById('lang-current');
var flag = document.getElementById('lang-flag');
if (label) {
var labels = {
it: 'Italiano',
en: 'English',
en_us: 'English USA',
de: 'Deutsch',
fr: 'Français',
de_ch: 'Deutsch CH',
fr_ch: 'Français CH'
};
label.textContent = labels[lang] || 'Italiano';
}
if (flag) {
var flags = {
it: '/static/vendor/flags/it.svg',
en: '/static/vendor/flags/en.svg',
en_us: '/static/vendor/flags/en_us.svg',
de: '/static/vendor/flags/de.svg',
fr: '/static/vendor/flags/fr.svg',
de_ch: '/static/vendor/flags/ch.svg',
fr_ch: '/static/vendor/flags/ch.svg'
};
var labels = {
it: 'Italiano',
en: 'English',
en_us: 'English USA',
de: 'Deutsch',
fr: 'Français',
de_ch: 'Deutsch CH',
fr_ch: 'Français CH'
};
flag.src = flags[lang] || '/static/vendor/flags/it.svg';
flag.alt = labels[lang] || 'Lingua';
if (lang === 'de_ch' || lang === 'fr_ch') {
flag.style.width = '32px';
flag.style.height = '32px';
} else {
flag.style.width = '48px';
flag.style.height = '32px';
}
}
(root || document).querySelectorAll('[data-i18n]').forEach(function (el) {
el.textContent = t(el.getAttribute('data-i18n'), lang);
});
(root || document).querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder'), lang));
});
(root || document).querySelectorAll('[data-localize-date]').forEach(function (el) {
var rawValue = el.getAttribute('data-localize-date');
el.textContent = localizeDate(rawValue, lang);
});
}
document.querySelectorAll('[data-lang-select]').forEach(function (btn) {
btn.addEventListener('click', function () {
localStorage.setItem(STORAGE_KEY, btn.getAttribute('data-lang-select'));
applyTranslations(document);
var dropdownInstance = window.FlowbiteInstances && window.FlowbiteInstances.getInstance
? window.FlowbiteInstances.getInstance('Dropdown', 'lang-dropdown')
: null;
if (dropdownInstance && typeof dropdownInstance.hide === 'function') {
dropdownInstance.hide();
return;
}
var langButton = document.getElementById('lang-menu-button');
var langDropdown = document.getElementById('lang-dropdown');
if (langDropdown) langDropdown.classList.add('hidden');
if (langButton) langButton.setAttribute('aria-expanded', 'false');
});
});
applyTranslations(document);
document.body.addEventListener('htmx:afterSwap', function (evt) {
applyTranslations(evt.target || document);
});
})();
</script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{{define "language_dropdown"}}
<div class="relative flex items-center gap-2">
<img id="lang-flag" class="rounded object-cover" src="/static/vendor/flags/it.svg" alt="Italiano" style="width:48px;height:32px;">
<button id="lang-menu-button" data-dropdown-toggle="lang-dropdown" type="button" class="inline-flex h-10 items-center rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200" aria-expanded="false">
<span id="lang-current" class="inline-block min-w-[95px]">Italiano</span>
</button>
<div id="lang-dropdown" class="z-50 my-2 hidden w-40 list-none divide-y divide-gray-100 rounded-lg bg-white text-sm shadow-sm">
<ul class="py-1" aria-labelledby="lang-menu-button">
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="it"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang rounded-sm object-cover" src="/static/vendor/flags/it.svg" alt=""></div><span class="leading-none">Italiano</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="en"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang rounded-sm object-cover" src="/static/vendor/flags/en.svg" alt=""></div><span class="leading-none">English</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="en_us"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang rounded-sm object-cover" src="/static/vendor/flags/en_us.svg" alt=""></div><span class="leading-none">English USA</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="de"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang rounded-sm object-cover" src="/static/vendor/flags/de.svg" alt=""></div><span class="leading-none">Deutsch</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="de_ch"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang-ch rounded-sm object-cover" src="/static/vendor/flags/ch.svg" alt=""></div><span class="leading-none">Deutsch CH</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="fr"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang rounded-sm object-cover" src="/static/vendor/flags/fr.svg" alt=""></div><span class="leading-none">Français</span></button></li>
<li><button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-left hover:bg-gray-100" data-lang-select="fr_ch"><div class="flex w-8 shrink-0 items-center justify-center"><img class="flag-lang-ch rounded-sm object-cover" src="/static/vendor/flags/ch.svg" alt=""></div><span class="leading-none">Français CH</span></button></li>
</ul>
</div>
</div>
{{end}}

View File

@ -0,0 +1,52 @@
{{define "navbar"}}
<nav class="border-b border-gray-200 bg-white">
<div class="mx-auto flex max-w-7xl flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a>
<button data-collapse-toggle="navbar-main" type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 md:hidden" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only" data-i18n="nav.open_main_menu">Apri menu principale</span>
<svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto" id="navbar-main">
<ul class="mt-4 flex flex-col gap-2 rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium md:mt-0 md:flex-row md:items-center md:gap-1 md:border-0 md:bg-transparent md:p-0">
<li><a href="/welcome" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100">Welcome</a></li>
</ul>
<div class="mt-4 flex items-center gap-3 md:mt-0 md:ms-4">
{{template "navbar_controls" .}}
</div>
</div>
</div>
</nav>
{{end}}
{{define "navbar_controls"}}
{{template "language_dropdown" .}}
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm" id="user-dropdown">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500">{{.CurrentUser.Email}}</span>
</div>
<div class="py-2">
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50" data-i18n="nav.logout">Logout</button>
</form>
</div>
</div>
</div>
{{end}}
{{end}}

View File

@ -1,49 +0,0 @@
{{define "content"}}
<section class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-gray-900">Users</h1>
<button type="button" class="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800" data-modal-target="privateUserModal" data-modal-toggle="privateUserModal">Nuovo Utente</button>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<label for="private-users-search" class="mb-2 block text-sm font-medium text-gray-900">Search</label>
<input id="private-users-search" type="text" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900" placeholder="Cerca utenti">
</div>
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
<table class="w-full text-left text-sm text-gray-500">
<thead class="bg-gray-50 text-xs uppercase text-gray-700">
<tr>
<th class="px-6 py-3">Name</th>
<th class="px-6 py-3">Email</th>
<th class="px-6 py-3">Role</th>
</tr>
</thead>
<tbody>
<tr class="border-b bg-white hover:bg-gray-50">
<td class="px-6 py-4">-</td>
<td class="px-6 py-4">-</td>
<td class="px-6 py-4"><span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">user</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<div id="privateUserModal" tabindex="-1" aria-hidden="true" class="fixed left-0 right-0 top-0 z-50 hidden h-[calc(100%-1rem)] max-h-full w-full items-center justify-center overflow-y-auto overflow-x-hidden p-4 md:inset-0">
<div class="relative max-h-full w-full max-w-xl">
<div class="relative rounded-lg bg-white shadow-sm">
<div class="flex items-start justify-between rounded-t border-b p-4 md:p-5">
<h3 class="text-xl font-semibold text-gray-900">Nuovo utente</h3>
<button type="button" class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900" data-modal-hide="privateUserModal" aria-label="Close modal">
<span class="sr-only">Close modal</span>
<svg class="h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" /></svg>
</button>
</div>
<div class="p-4 md:p-5">
<p class="text-sm text-gray-600">Template Flowbite pronto per integrazione backend.</p>
</div>
</div>
</div>
</div>
{{end}}

View File

@ -1,19 +1,21 @@
{{define "content"}}
<section class="grid gap-6 md:grid-cols-3">
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:col-span-2">
<h1 class="mb-2 text-2xl font-bold text-gray-900">Dashboard</h1>
<h1 class="mb-2 text-2xl font-bold text-gray-900">welcome</h1>
{{if .CurrentUser}}
<p class="text-gray-600">Bentornato {{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}{{.CurrentUser.Email}}{{end}}.</p>
<p class="text-gray-600"><span data-i18n="welcome.back_prefix">Bentornato</span> {{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}{{.CurrentUser.Email}}{{end}}.</p>
{{else}}
<p class="text-gray-600">Benvenuto.</p>
<p class="text-gray-600" data-i18n="welcome.generic">Benvenuto.</p>
{{end}}
</article>
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="mb-3 text-lg font-semibold text-gray-900">Quick Links</h2>
<h2 class="mb-3 text-lg font-semibold text-gray-900" data-i18n="welcome.quick_links">Quick Links</h2>
<div class="space-y-2">
<a href="/welcome" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
<a href="/admin/users" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">Users</a>
<a href="/welcome" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">welcome</a>
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
<a href="/admin/users" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100" data-i18n="nav.users">Users</a>
{{end}}
</div>
</article>
</section>

View File

@ -0,0 +1,61 @@
{{define "navbar"}}
<nav class="border-b border-gray-200 bg-white">
<div class="mx-auto flex max-w-7xl flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a>
<button data-collapse-toggle="navbar-main" type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 md:hidden" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only" data-i18n="nav.open_main_menu">Apri menu principale</span>
<svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto" id="navbar-main">
<ul class="mt-4 flex flex-col gap-2 rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium md:mt-0 md:flex-row md:items-center md:gap-1 md:border-0 md:bg-transparent md:p-0">
{{if eq .NavSection "home"}}
{{if .CurrentUser}}
<li><a href="/welcome" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100">Welcome</a></li>
{{else}}
<li><a href="/login" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100" data-i18n="nav.login">Login</a></li>
{{end}}
{{else if not .CurrentUser}}
<li><a href="/login" class="block rounded-lg px-3 py-2 {{if eq .NavSection "login"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}" data-i18n="nav.login">Login</a></li>
<li><a href="/signup" class="block rounded-lg px-3 py-2 {{if eq .NavSection "signup"}}bg-blue-100 text-blue-700{{else}}text-gray-700 hover:bg-gray-100{{end}}" data-i18n="nav.signup">Signup</a></li>
{{end}}
</ul>
<div class="mt-4 flex items-center gap-3 md:mt-0 md:ms-4">
{{template "navbar_controls" .}}
</div>
</div>
</div>
</nav>
{{end}}
{{define "navbar_controls"}}
{{template "language_dropdown" .}}
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm" id="user-dropdown">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500">{{.CurrentUser.Email}}</span>
</div>
<div class="py-2">
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50" data-i18n="nav.logout">Logout</button>
</form>
</div>
</div>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,9 @@
{{define "content"}}
<div class="flex min-h-screen items-center justify-center">
<div class="w-full max-w-2xl rounded-lg border border-red-200 bg-white p-8 shadow-sm">
<h1 class="mb-3 text-2xl font-bold text-red-700">Accesso negato</h1>
<p class="mb-6 text-gray-700">Non hai i privilegi necessari per accedere a questa pagina.</p>
<a href="/" class="inline-flex rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800">Torna alla home</a>
</div>
</div>
{{end}}

View File

@ -1,18 +1,18 @@
{{define "content"}}
<div class="flex min-h-screen items-center justify-center">
<div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<h1 class="mb-1 text-2xl font-bold text-gray-900">Forgot Password</h1>
<p class="mb-6 text-sm text-gray-500">Inserisci la tua email per ricevere il link di reset.</p>
<h1 class="mb-1 text-2xl font-bold text-gray-900" data-i18n="forgot.title">Forgot Password</h1>
<p class="mb-6 text-sm text-gray-500" data-i18n="forgot.subtitle">Inserisci la tua email per ricevere il link di reset.</p>
<form action="/forgot-password" method="post" class="space-y-5">
<div>
<label for="forgot-email" class="mb-2 block text-sm font-medium text-gray-900">Email</label>
<label for="forgot-email" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.email">Email</label>
<input id="forgot-email" type="email" name="email" value="{{.Email}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">Invia link reset</button>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="forgot.submit">Invia link reset</button>
<p class="text-center text-sm text-gray-600"><a href="/login" class="text-blue-700 hover:underline">Torna al login</a></p>
<p class="text-center text-sm text-gray-600"><a href="/login" class="text-blue-700 hover:underline" data-i18n="forgot.back_login">Torna al login</a></p>
</form>
</div>
</div>

View File

@ -1,10 +1,5 @@
{{define "content"}}
<section class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
<h1 class="mb-2 text-3xl font-bold text-gray-900">Trustcontact</h1>
<p class="mb-6 text-gray-600">Accedi o registrati per continuare.</p>
<div class="flex flex-wrap gap-3">
<a class="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800" href="/login">Accedi</a>
<a class="rounded-lg border border-gray-300 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100" href="/signup">Registrati</a>
</div>
<h1 class="text-3xl font-bold text-gray-900">Home Page</h1>
</section>
{{end}}

View File

@ -1,25 +1,25 @@
{{define "content"}}
<div class="flex min-h-screen items-center justify-center">
<div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<h1 class="mb-1 text-2xl font-bold text-gray-900">Login</h1>
<p class="mb-6 text-sm text-gray-500">Accedi al tuo account.</p>
<h1 class="mb-1 text-2xl font-bold text-gray-900" data-i18n="login.title">Login</h1>
<p class="mb-6 text-sm text-gray-500" data-i18n="login.subtitle">Accedi al tuo account.</p>
<form action="/login" method="post" class="space-y-5">
<div>
<label for="email" class="mb-2 block text-sm font-medium text-gray-900">Email</label>
<label for="email" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.email">Email</label>
<input id="email" type="text" name="email" value="{{.Email}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<div>
<label for="password" class="mb-2 block text-sm font-medium text-gray-900">Password</label>
<label for="password" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.password">Password</label>
<input id="password" type="password" name="password" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">Sign in</button>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="login.submit">Sign in</button>
<div class="flex items-center justify-between text-sm">
<a href="/forgot-password" class="text-blue-700 hover:underline">Forgot password?</a>
<a href="/signup" class="text-gray-600 hover:underline">Create account</a>
<a href="/forgot-password" class="text-blue-700 hover:underline" data-i18n="login.forgot">Forgot password?</a>
<a href="/signup" class="text-gray-600 hover:underline" data-i18n="login.create_account">Create account</a>
</div>
</form>
</div>

View File

@ -1,20 +1,20 @@
{{define "content"}}
<div class="flex min-h-screen items-center justify-center">
<div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<h1 class="mb-1 text-2xl font-bold text-gray-900">Reset Password</h1>
<p class="mb-6 text-sm text-gray-500">Imposta una nuova password.</p>
<h1 class="mb-1 text-2xl font-bold text-gray-900" data-i18n="reset.title">Reset Password</h1>
<p class="mb-6 text-sm text-gray-500" data-i18n="reset.subtitle">Imposta una nuova password.</p>
{{if .Token}}
<form action="/reset-password?token={{.Token}}" method="post" class="space-y-5">
<div>
<label for="reset-password" class="mb-2 block text-sm font-medium text-gray-900">Nuova password</label>
<label for="reset-password" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="reset.new_password">Nuova password</label>
<input id="reset-password" type="password" name="password" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">Aggiorna password</button>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="reset.submit">Aggiorna password</button>
</form>
{{else}}
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" role="alert">Token mancante o non valido.</div>
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" role="alert" data-i18n="reset.invalid_token">Token mancante o non valido.</div>
{{end}}
</div>
</div>

View File

@ -1,23 +1,23 @@
{{define "content"}}
<div class="flex min-h-screen items-center justify-center">
<div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<h1 class="mb-1 text-2xl font-bold text-gray-900">Signup</h1>
<p class="mb-6 text-sm text-gray-500">Crea il tuo account.</p>
<h1 class="mb-1 text-2xl font-bold text-gray-900" data-i18n="signup.title">Signup</h1>
<p class="mb-6 text-sm text-gray-500" data-i18n="signup.subtitle">Crea il tuo account.</p>
<form action="/signup" method="post" class="space-y-5">
<div>
<label for="signup-email" class="mb-2 block text-sm font-medium text-gray-900">Email</label>
<label for="signup-email" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.email">Email</label>
<input id="signup-email" type="email" name="email" value="{{.Email}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<div>
<label for="signup-password" class="mb-2 block text-sm font-medium text-gray-900">Password</label>
<label for="signup-password" class="mb-2 block text-sm font-medium text-gray-900" data-i18n="form.password">Password</label>
<input id="signup-password" type="password" name="password" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500" required />
</div>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300">Sign up</button>
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="signup.submit">Sign up</button>
<p class="text-center text-sm text-gray-600">Hai già un account? <a href="/login" class="text-blue-700 hover:underline">Accedi</a></p>
<p class="text-center text-sm text-gray-600"><span data-i18n="signup.has_account">Hai già un account?</span> <a href="/login" class="text-blue-700 hover:underline" data-i18n="signup.login">Accedi</a></p>
</form>
</div>
</div>

View File

@ -1,8 +1,8 @@
{{define "content"}}
<section class="rounded-lg border border-blue-200 bg-blue-50 p-8 shadow-sm" role="status" aria-live="polite">
<h1 class="mb-2 text-2xl font-bold text-blue-900">Verifica email</h1>
<p class="mb-2 text-blue-800">Controlla la casella di posta e apri il link di verifica ricevuto.</p>
<p class="mb-4 text-blue-800">Se il link è scaduto, ripeti la registrazione o contatta supporto.</p>
<a href="/login" class="inline-flex rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800">Vai al login</a>
<h1 class="mb-2 text-2xl font-bold text-blue-900" data-i18n="verify.title">Verifica email</h1>
<p class="mb-2 text-blue-800" data-i18n="verify.p1">Controlla la casella di posta e apri il link di verifica ricevuto.</p>
<p class="mb-4 text-blue-800" data-i18n="verify.p2">Se il link è scaduto, ripeti la registrazione o contatta supporto.</p>
<a href="/login" class="inline-flex rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800" data-i18n="verify.go_login">Vai al login</a>
</section>
{{end}}