adattato html, test htmx con componente svelte

This commit is contained in:
fabio
2026-02-22 20:23:21 +01:00
parent 0cd6ce05cd
commit 83e85bf899
24 changed files with 3705 additions and 1491 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
<p class="muted">Area amministrazione.</p>
<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="/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Vista utenti (private)</a>
</div>
</div>
{{end}}

View File

@@ -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}}

View File

@@ -4,13 +4,13 @@
<thead>
<tr>
<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 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 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 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>
@@ -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;">
<button
hx-get="/users/{{$u.ID}}/modal"
hx-target="#userModal"
hx-get="/admin/users/{{$u.ID}}/modal"
hx-target="#userModalContent"
hx-swap="innerHTML"
hx-on::after-request="document.getElementById('userModal').setAttribute('open','')"
>Apri</button>
</td>
</tr>
@@ -41,8 +40,8 @@
<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="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.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.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="/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>
{{end}}

View File

@@ -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}}

View File

@@ -10,17 +10,26 @@
<script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
</head>
<body>
<nav class="flex gap-3 bg-slate-900 px-4 py-3 text-white">
<a href="/" class="text-slate-200 hover:text-white {{if eq .NavSection "public"}}font-semibold text-white{{end}}">Public</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")}}
<a href="/admin" class="text-slate-200 hover:text-white {{if eq .NavSection "admin"}}font-semibold text-white{{end}}">Admin</a>
{{end}}
{{if .CurrentUser}}
<form action="/logout" method="post" class="ml-auto">
<button type="submit" class="btn-primary">Logout</button>
</form>
{{end}}
<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-lg font-semibold text-slate-800">Trustcontact</a>
<div class="hidden items-center gap-8 sm:flex">
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
<a href="/admin" class="text-slate-700 hover:text-slate-900 {{if eq .NavSection "admin"}}font-semibold{{end}}">Admin</a>
{{end}}
{{if .CurrentUser}}
<form action="/logout" method="post">
<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>
<div class="mx-auto my-5 max-w-5xl px-4">

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -1,9 +1,25 @@
{{define "content"}}
<h1>Password dimenticata</h1>
<p class="muted">Inserisci la tua email. Se l'account esiste e risulta verificato, invieremo un link di reset.</p>
<form action="/forgot-password" method="post">
<label>Email</label>
<input type="email" name="email" value="{{.Email}}" required>
<button type="submit">Invia link reset</button>
</form>
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-amber-100">
<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">
<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>
</svg>
</div>
<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}}

View File

@@ -1,9 +1,10 @@
{{define "content"}}
<h1>Trustcontact</h1>
<p class="muted">Boilerplate GoFiber + HTMX + Svelte CE + GORM.</p>
<div class="row">
<a href="/signup">Crea account</a>
<a href="/login">Accedi</a>
<a href="/forgot-password">Password dimenticata</a>
<div class="space-y-3">
<h1 class="text-2xl font-semibold">Trustcontact</h1>
<p class="muted">Accedi o registrati per continuare.</p>
<div class="row">
<a class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50" href="/login">Accedi</a>
<a class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50" href="/signup">Registrati</a>
</div>
</div>
{{end}}

View File

@@ -1,11 +1,32 @@
{{define "content"}}
<h1>Login</h1>
<form action="/login" method="post">
<label>Email</label>
<input type="email" name="email" value="{{.Email}}" required>
<label>Password</label>
<input type="password" name="password" required>
<button type="submit">Accedi</button>
</form>
<p class="muted">Non hai un account? <a href="/signup">Registrati</a></p>
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<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">
<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>
</svg>
</div>
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Quick Login</h3>
<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}}

View File

@@ -1,12 +1,24 @@
{{define "content"}}
<h1>Reset password</h1>
{{if .Token}}
<form action="/reset-password?token={{.Token}}" method="post">
<label>Nuova password</label>
<input type="password" name="password" required>
<button type="submit">Aggiorna password</button>
</form>
{{else}}
<p class="muted">Token mancante o non valido.</p>
{{end}}
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-violet-100">
<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">
<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>
</svg>
</div>
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Reset Password</h3>
{{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}}

View File

@@ -1,11 +1,29 @@
{{define "content"}}
<h1>Sign up</h1>
<form action="/signup" method="post">
<label>Email</label>
<input type="email" name="email" value="{{.Email}}" required>
<label>Password</label>
<input type="password" name="password" required>
<button type="submit">Crea account</button>
</form>
<p class="muted">Hai già un account? <a href="/login">Accedi</a></p>
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100">
<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">
<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>
</svg>
</div>
<h3 class="mb-6 text-center text-xl font-bold text-gray-800">Create Account</h3>
<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}}