aggiornato per uso di taiwind

This commit is contained in:
fabio 2026-02-23 13:46:44 +01:00
parent 275d3df3f1
commit 3fc01cc4f7
39 changed files with 5743 additions and 1381 deletions

4
.gitignore vendored
View File

@ -35,3 +35,7 @@ ui-kit/node_modules/
*.sqlite3 *.sqlite3
*.db *.db
/data/emails/ /data/emails/
# Root JS deps
node_modules/
flowbite-ui/node_modules/

View File

@ -1,20 +1,22 @@
.PHONY: dev ui-build ui-dev css-build css-dev test db-reset fmt .PHONY: tw-build tw-watch htmx-copy flowbite-copy assets server test db-reset fmt
dev: tw-build:
npm --prefix flowbite-ui run tw:build
tw-watch:
npm --prefix flowbite-ui run tw:watch
htmx-copy:
mkdir -p web/static/vendor && cp flowbite-ui/node_modules/htmx.org/dist/htmx.min.js web/static/vendor/htmx.min.js
flowbite-copy:
mkdir -p web/static/vendor && cp flowbite-ui/node_modules/flowbite/dist/flowbite.min.js web/static/vendor/flowbite.js
assets: htmx-copy flowbite-copy tw-build
server:
go run ./cmd/server go run ./cmd/server
ui-build:
cd ui-kit && npm i && npm run build && npm run css:build
ui-dev:
cd ui-kit && npm i && npm run dev
css-build:
cd ui-kit && npm i && npm run css:build
css-dev:
cd ui-kit && npm i && npm run css:dev
test: test:
go test ./... go test ./...

View File

@ -1,14 +1,34 @@
# GoFiber MVC Boilerplate # GoFiber MVC Boilerplate
Boilerplate GoFiber MVC + HTMX + Svelte Custom Elements + GORM, con auth server-rendered, area private/admin e mail sink in sviluppo. Boilerplate GoFiber MVC + HTMX + Flowbite + GORM, con auth server-rendered, area private/admin e mail sink in sviluppo.
## Setup Assets + Server
Terminale 1:
```bash
npm i --prefix flowbite-ui
make assets
make tw-watch
```
Terminale 2:
```bash
make server
```
`make assets` esegue:
- copia di `flowbite-ui/node_modules/flowbite/dist/flowbite.min.js` in `web/static/vendor/flowbite.js`
- build Tailwind in `web/static/css/app.css`
## Quickstart SQLite ## Quickstart SQLite
```bash ```bash
cp .env.example .env cp .env.example .env
make css-build npm i --prefix flowbite-ui
make ui-build make assets
make dev make server
``` ```
Default SQLite path: `./data/app.sqlite3`. Default SQLite path: `./data/app.sqlite3`.
@ -37,32 +57,12 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
`DB_POSTGRES_DSN` è comunque supportato. `DB_POSTGRES_DSN` è comunque supportato.
## Tailwind + UI Kit
Tailwind (template server-rendered) compila in `web/static/css/app.css`.
UI kit (Svelte custom elements) compila in `web/static/ui`.
Comandi:
```bash
make css-build # build tailwind
make css-dev # watch tailwind
make ui-build # build ui-kit + css tailwind
make ui-dev # vite dev server ui-kit
```
Layout include:
- `/static/css/app.css?v={{.BuildHash}}`
- `/static/ui/ui.css?v={{.BuildHash}}`
- `/static/ui/ui.esm.js?v={{.BuildHash}}`
## Template Directories ## Template Directories
- Public: `web/templates/public` - Public: `web/templates/public`
- Private: `web/templates/private` - Private: `web/templates/private`
- Admin: `web/templates/admin` - Admin: `web/templates/admin`
- Components Flowbite: `web/templates/components`
## Email in Develop ## Email in Develop
@ -70,11 +70,25 @@ In `develop`, le email vengono salvate in `./data/emails`.
## Make Targets ## Make Targets
- `make dev` -> `go run ./cmd/server` - `make tw-build` -> build Tailwind CSS
- `make ui-build` -> install + build ui-kit + build css tailwind - `make tw-watch` -> watch Tailwind CSS
- `make ui-dev` -> watch UI con Vite - `make flowbite-copy` -> copia `flowbite-ui/node_modules/flowbite/dist/flowbite.min.js` in `web/static/vendor/flowbite.js`
- `make css-build` -> build Tailwind CSS - `make assets` -> `flowbite-copy` + `tw-build`
- `make css-dev` -> watch Tailwind CSS - `make server` -> `go run ./cmd/server`
- `make test` -> `go test ./...` - `make test` -> `go test ./...`
- `make db-reset` -> reset DB sqlite locale (`./data/app.db` / `./data/app.sqlite3`) - `make db-reset` -> reset DB sqlite locale (`./data/app.db` / `./data/app.sqlite3`)
- `make fmt` -> `gofmt` su `cmd/` e `internal/` - `make fmt` -> `gofmt` su `cmd/` e `internal/`
# Third-Party Notices
This project uses third-party software distributed under the MIT License.
## Flowbite
- Package: `flowbite`
- License: MIT
- Upstream: https://github.com/themesberg/flowbite
- Full text: `licenses/FLOWBITE-MIT.txt`
## Tailwind CSS
- Packages: `tailwindcss`, `@tailwindcss/cli`
- License: MIT
- Upstream: https://github.com/tailwindlabs/tailwindcss

View File

@ -0,0 +1 @@
@import "../../flowbite-ui/node_modules/tailwindcss/index.css";

View File

@ -0,0 +1,71 @@
TASK: Integra Flowbite (UI + JS behavior) e aggiungi Makefile per Tailwind CLI (build/watch). Uso più terminali: non aggiungere tool per runner multi-process.
1) Dipendenze Node
- Se non esiste package.json in root, crearlo.
- Installare:
- npm install -D @tailwindcss/cli tailwindcss
- npm install flowbite
2) Tailwind config
- Creare tailwind.config.js con:
content: [
"./web/templates/**/*.{html,gohtml}",
"./web/static/**/*.js",
"./node_modules/flowbite/**/*.js"
],
theme: { extend: {} },
plugins: [ require("flowbite/plugin") ]
3) CSS input
- Creare ./assets/tailwind/input.css con:
@import "tailwindcss";
4) Scripts npm
- In package.json aggiungere:
"scripts": {
"tw:build": "npx @tailwindcss/cli -i ./assets/tailwind/input.css -o ./web/static/css/app.css --minify",
"tw:watch": "npx @tailwindcss/cli -i ./assets/tailwind/input.css -o ./web/static/css/app.css --watch"
}
5) Copia Flowbite JS
- Creare directory web/static/vendor se manca
- Copiare:
node_modules/flowbite/dist/flowbite.min.js -> web/static/vendor/flowbite.js
6) Layout
- Aggiornare /web/templates/layout.html per includere:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/flowbite.js"></script>
- Rimuovere dal layout riferimenti attivi al vecchio UI kit Svelte (ma non cancellare /ui-kit dal repo)
7) Makefile
- Creare/aggiornare Makefile con target:
- tw-build: npm run tw:build
- tw-watch: npm run tw:watch
- flowbite-copy: mkdir -p web/static/vendor && cp node_modules/flowbite/dist/flowbite.min.js web/static/vendor/flowbite.js
- assets: flowbite-copy tw-build
- server: go run ./cmd/server
- Non creare target che avvia più processi insieme.
8) Partials Flowbite
- Creare /web/templates/components:
- navbar.html
- modal.html
- dropdown.html
- tabs.html
- collapse.html
Con markup Flowbite + data-attributes standard, senza JS custom.
9) Licenze
- Creare /licenses/FLOWBITE-MIT.txt e THIRD_PARTY_NOTICES.md (Flowbite MIT, Tailwind MIT).
10) README
- Aggiornare README con istruzioni:
Terminale 1: npm i && make assets && make tw-watch
Terminale 2: make server
Criteri:
- make assets genera CSS e copia flowbite.js
- modals/dropdowns Flowbite funzionano
- progetto compilabile e avviabile.

View File

@ -0,0 +1,141 @@
TASK: Convertire tutti i template HTML esistenti per usare componenti Flowbite (markup + behavior JS) mantenendo logica MVC e HTMX.
Non modificare controller, services, repo.
Modificare solo template e layout.
-------------------------------------
1) LAYOUT GLOBALE
-------------------------------------
Aggiornare /web/templates/layout.html:
- Layout container moderno Tailwind
- Navbar Flowbite responsive con:
- Logo/AppName a sinistra
- Link:
- Public: Login / Signup
- Private: Dashboard / Users
- Admin: Admin (solo se role=admin)
- Dropdown utente con logout
- Include:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/flowbite.js"></script>
Struttura:
- Navbar top
- Container max-w-7xl mx-auto p-6
- Footer minimale
-------------------------------------
2) PUBLIC TEMPLATES
-------------------------------------
Convertire:
/web/templates/public/login.html
/web/templates/public/signup.html
/web/templates/public/forgot_password.html
/web/templates/public/reset_password.html
Usare:
- Card Flowbite per form
- Input Flowbite style
- Button primary
- Alert Flowbite per flash messages
- Layout centrato verticalmente (flex items-center justify-center min-h-screen)
-------------------------------------
3) PRIVATE USERS
-------------------------------------
/web/templates/private/users/index.html
- Header sezione con:
- Titolo
- Pulsante "Nuovo Utente" (modal Flowbite)
- Search input Flowbite
- Table Flowbite styled (striped, hover)
- Pagination button Flowbite
- Modal Flowbite per dettaglio utente
Assicurarsi che:
- hx-get
- hx-target
- hx-swap
rimangano funzionanti
-------------------------------------
4) ADMIN
-------------------------------------
/web/templates/admin/dashboard.html
/web/templates/admin/users.html
/web/templates/admin/audit_logs.html
Usare:
- Card summary (stats)
- Table Flowbite
- Badge per ruoli (admin = red, user = blue)
- Tabs Flowbite se presenti più sezioni
-------------------------------------
5) FLASH MESSAGES
-------------------------------------
Creare partial:
- /web/templates/components/flash.html
Usare Flowbite alert component:
- success -> green
- error -> red
- warning -> yellow
Includere in layout sopra {{embed}}
-------------------------------------
6) MODAL STANDARD
-------------------------------------
Creare partial riusabile:
/web/templates/components/modal.html
Markup Flowbite standard con:
- id dinamico
- header con titolo
- body slot
- footer slot opzionale
- data-modal-toggle attributes
Usare nelle pagine private.
-------------------------------------
7) ACCESSIBILITÀ
-------------------------------------
- Usare aria attributes corretti come in documentazione Flowbite
- Non rompere keyboard interaction
- Mantenere form method e csrf se presente
-------------------------------------
8) RESPONSIVE DESIGN
-------------------------------------
- Navbar collapse mobile
- Table scrollable mobile
- Forms full width mobile
-------------------------------------
9) CRITERI DI ACCETTAZIONE
-------------------------------------
- Tutte le pagine hanno layout moderno Flowbite
- Modals funzionano
- Dropdown funzionano
- Navbar responsive
- HTMX partial update continua a funzionare
- Nessuna modifica backend richiesta
- Nessun errore JS in console
Scrivere codice pulito, leggibile, con commenti minimi.
Non eliminare logica Go template esistente.

5
flowbite-ui/input.css Normal file
View File

@ -0,0 +1,5 @@
@config "./tailwind.config.js";
@source "../web/templates/**/*.{html,gohtml}";
@source "../web/static/**/*.js";
@source "./node_modules/flowbite/**/*.js";
@import "tailwindcss";

1312
flowbite-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
flowbite-ui/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "trustcontact-flowbite",
"private": true,
"version": "1.0.0",
"scripts": {
"tw:build": "npx @tailwindcss/cli -i ./input.css -o ../web/static/css/app.css --minify",
"tw:watch": "npx @tailwindcss/cli -i ./input.css -o ../web/static/css/app.css --watch"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.13",
"tailwindcss": "^4.1.13"
},
"dependencies": {
"flowbite": "^3.1.2",
"htmx.org": "^2.0.6"
}
}

View File

@ -0,0 +1,9 @@
module.exports = {
content: [
"../web/templates/**/*.{html,gohtml}",
"../web/static/**/*.js",
"./node_modules/flowbite/**/*.js"
],
theme: { extend: {} },
plugins: [require("flowbite/plugin")]
};

21
licenses/FLOWBITE-MIT.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Themesberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

2
web/static/vendor/flowbite.js vendored Normal file

File diff suppressed because one or more lines are too long

1
web/static/vendor/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
{{define "content"}}
<section class="space-y-6">
<h1 class="text-3xl font-bold text-gray-900">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>
</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>
</li>
</ul>
</div>
<div id="audit-tab-content">
<div class="hidden rounded-lg border border-gray-200 bg-white p-4 shadow-sm" id="activity" role="tabpanel" aria-labelledby="activity-tab">
<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>
</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>
</div>
</div>
</section>
{{end}}

View File

@ -1,9 +1,29 @@
{{define "content"}} {{define "content"}}
<div class="space-y-3"> <section class="space-y-6">
<h1 class="text-2xl font-semibold">Admin Dashboard</h1> <div>
<p class="muted">Area amministrazione.</p> <h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<div class="row"> <p class="text-gray-600">Area amministrazione.</p>
<a href="/admin/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Gestione utenti</a>
</div> </div>
</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="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="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>
{{else}}
<span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">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>
</article>
</div>
</section>
{{end}} {{end}}

View File

@ -0,0 +1,8 @@
{{define "content"}}
<section class="space-y-4">
<h1 class="text-3xl font-bold text-gray-900">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>
</div>
</section>
{{end}}

View File

@ -1,13 +1,36 @@
{{define "users_modal"}} {{define "users_modal"}}
<div style="padding:16px;"> <div class="grid gap-3 text-sm text-gray-700 sm:grid-cols-2">
<h3 style="margin-top:0;">Dettaglio utente #{{.User.ID}}</h3> <div>
<p><strong>Name:</strong> {{if .User.Name}}{{.User.Name}}{{else}}-{{end}}</p> <span class="font-semibold text-gray-900">ID:</span>
<p><strong>Email:</strong> {{.User.Email}}</p> <span>{{.User.ID}}</span>
<p><strong>Role:</strong> {{.User.Role}}</p> </div>
<p><strong>Verified:</strong> {{if .User.EmailVerified}}yes{{else}}no{{end}}</p> <div>
<p><strong>Created:</strong> {{.User.CreatedAt}}</p> <span class="font-semibold text-gray-900">Role:</span>
<div class="row"> {{if eq .User.Role "admin"}}
<button type="button" onclick="document.getElementById('userModal').removeAttribute('open')">Chiudi</button> <span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">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>
{{end}}
</div>
<div class="sm:col-span-2">
<span class="font-semibold text-gray-900">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>{{.User.Email}}</span>
</div>
<div>
<span class="font-semibold text-gray-900">Verified:</span>
<span>{{if .User.EmailVerified}}yes{{else}}no{{end}}</span>
</div>
<div>
<span class="font-semibold text-gray-900">Created:</span>
<span>{{.User.CreatedAt}}</span>
</div> </div>
</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>
</div>
{{end}} {{end}}

View File

@ -1,47 +1,53 @@
{{define "users_table"}} {{define "users_table"}}
{{ $p := .PageData }} {{ $p := .PageData }}
<table style="width:100%;border-collapse:collapse;margin-top:16px;"> <div class="relative overflow-x-auto">
<thead> <table class="w-full text-left text-sm text-gray-500 rtl:text-right">
<tr> <thead class="bg-gray-50 text-xs uppercase text-gray-700">
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;"> <tr>
<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 scope="col" class="px-6 py-3">
</th> <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>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;"> </th>
<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 scope="col" class="px-6 py-3">
</th> <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>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;"> </th>
<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 scope="col" class="px-6 py-3">
</th> <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>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Role</th> </th>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Azioni</th> <th scope="col" class="px-6 py-3">Role</th>
</tr> <th scope="col" class="px-6 py-3">Azioni</th>
</thead> </tr>
<tbody> </thead>
{{range $u := $p.Users}} <tbody>
<tr> {{range $u := $p.Users}}
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.ID}}</td> <tr class="border-b bg-white hover:bg-gray-50">
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{if $u.Name}}{{$u.Name}}{{else}}-{{end}}</td> <td class="px-6 py-4">{{$u.ID}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Email}}</td> <td class="px-6 py-4 font-medium text-gray-900">{{if $u.Name}}{{$u.Name}}{{else}}-{{end}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Role}}</td> <td class="px-6 py-4">{{$u.Email}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;"> <td class="px-6 py-4">
<button {{if eq $u.Role "admin"}}
hx-get="/admin/users/{{$u.ID}}/modal" <span class="rounded-sm bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800">admin</span>
hx-target="#userModalContent" {{else}}
hx-swap="innerHTML" <span class="rounded-sm bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">user</span>
>Apri</button> {{end}}
</td> </td>
</tr> <td class="px-6 py-4">
{{else}} <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>
<tr><td colspan="5" style="padding:12px;">Nessun utente trovato.</td></tr> </td>
{{end}} </tr>
</tbody> {{else}}
</table> <tr class="bg-white">
<td colspan="5" class="px-6 py-4 text-center text-gray-500">Nessun utente trovato.</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="row" style="margin-top:12px;align-items:center;justify-content:space-between;"> <div class="mt-4 flex flex-wrap items-center justify-between gap-3">
<div class="muted">Totale: {{$p.Total}} utenti. Pagina {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div> <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="row"> <div class="inline-flex gap-2">
<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 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 {{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.HasNext}}disabled{{end}} hx-get="/admin/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.NextPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Next</button>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -1,36 +1,74 @@
{{define "content"}} {{define "content"}}
<h1>Users</h1> <section class="space-y-6">
<p class="muted">Ricerca, ordinamento e paging server-side via HTMX.</p> <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>
</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">
Nuovo Utente
</button>
</div>
<form id="usersFilters" class="row" hx-get="/admin/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML"> <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">
<input type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}"> <div class="md:col-span-2">
<input type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" style="max-width:120px;"> <label for="users-q" class="mb-2 block text-sm font-medium text-gray-900">Search</label>
<input type="hidden" name="sort" value="{{.PageData.Sort}}"> <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">
<input type="hidden" name="dir" value="{{.PageData.Dir}}"> </div>
<input type="hidden" name="page" value="1"> <div>
<button type="submit">Cerca</button> <label for="users-size" class="mb-2 block text-sm font-medium text-gray-900">Page size</label>
</form> <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>
</div>
<input type="hidden" name="sort" value="{{.PageData.Sort}}">
<input type="hidden" name="dir" value="{{.PageData.Dir}}">
<input type="hidden" name="page" value="1">
</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"> <div id="usersTableContainer" class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm md:p-4" 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" .}} {{template "users_table" .}}
</div>
</section>
<div id="userModal" 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-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>
<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>
<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 id="userModalContent" class="space-y-4 p-4 md:p-5"></div>
</div>
</div>
</div> </div>
<ui-modal id="userModal" title="Dettaglio utente"> <div id="newUserModal" 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 <div class="relative max-h-full w-full max-w-xl">
id="userModalContent" <div class="relative rounded-lg bg-white shadow-sm">
hx-on:htmx:after-swap="document.getElementById('userModal').setAttribute('open','')" <div class="flex items-start justify-between rounded-t border-b p-4 md:p-5">
></div> <h3 class="text-xl font-semibold text-gray-900">Nuovo utente</h3>
</ui-modal> <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>
<script> <svg class="h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
(function () { <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" />
var modal = document.getElementById('userModal'); </svg>
var content = document.getElementById('userModalContent'); </button>
if (!modal || !content || modal.dataset.closeBound === '1') return; </div>
modal.dataset.closeBound = '1'; <div class="space-y-4 p-4 md:p-5">
modal.addEventListener('ui:close', function () { <p class="text-sm text-gray-600">Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.</p>
content.innerHTML = ''; <div>
}); <label for="new-user-email" class="mb-2 block text-sm font-medium text-gray-900">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>
</script> </div>
</div>
</div>
</div>
</div>
{{end}} {{end}}

View File

@ -0,0 +1,13 @@
{{define "flowbite_collapse"}}
<button data-collapse-toggle="collapseExample" type="button" class="flex w-full items-center justify-between rounded-lg bg-gray-100 px-5 py-2.5 text-left text-sm font-medium text-gray-500 hover:bg-gray-200" aria-expanded="false" aria-controls="collapseExample">
<span>Toggle collapse</span>
<svg data-accordion-icon class="h-3 w-3 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5 5 1 1 5"/>
</svg>
</button>
<div id="collapseExample" class="hidden">
<div class="rounded-b-lg border border-gray-200 p-5">
<p class="text-sm text-gray-500">Collapsed content.</p>
</div>
</div>
{{end}}

View File

@ -0,0 +1,16 @@
{{define "flowbite_dropdown"}}
<button id="dropdownDefaultButton" data-dropdown-toggle="dropdown" class="inline-flex items-center rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800" type="button">
Dropdown
<svg class="ms-3 h-2.5 w-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<div id="dropdown" class="z-10 hidden w-44 divide-y divide-gray-100 rounded-lg bg-white shadow-sm">
<ul class="py-2 text-sm text-gray-700" aria-labelledby="dropdownDefaultButton">
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">Dashboard</a></li>
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">Settings</a></li>
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">Sign out</a></li>
</ul>
</div>
{{end}}

View File

@ -0,0 +1,17 @@
{{define "flowbite_flash"}}
{{if .FlashSuccess}}
<div class="mb-4 flex items-center rounded-lg border border-green-200 bg-green-50 p-4 text-green-800" role="alert">
<span class="text-sm font-medium">{{.FlashSuccess}}</span>
</div>
{{end}}
{{if .FlashError}}
<div class="mb-4 flex items-center rounded-lg border border-red-200 bg-red-50 p-4 text-red-800" role="alert">
<span class="text-sm font-medium">{{.FlashError}}</span>
</div>
{{end}}
{{if .FlashWarning}}
<div class="mb-4 flex items-center rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-yellow-800" role="alert">
<span class="text-sm font-medium">{{.FlashWarning}}</span>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,25 @@
{{define "flowbite_modal"}}
<div id="{{.ModalID}}" 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-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">{{.ModalTitle}}</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="{{.ModalID}}" 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="space-y-4 p-4 md:p-5">
{{template .ModalBodyTemplate .}}
</div>
{{if .ModalFooterTemplate}}
<div class="flex items-center rounded-b border-t border-gray-200 p-4 md:p-5">
{{template .ModalFooterTemplate .}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}

View File

@ -0,0 +1,21 @@
{{define "flowbite_navbar"}}
<nav class="border-gray-200 bg-white">
<div class="mx-auto flex max-w-screen-xl 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-2xl font-semibold">Trustcontact</span>
</a>
<button data-collapse-toggle="navbar-default" 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-default" aria-expanded="false">
<span class="sr-only">Open main menu</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 md:block md:w-auto" id="navbar-default">
<ul class="mt-4 flex flex-col rounded-lg border border-gray-100 bg-gray-50 p-4 font-medium md:mt-0 md:flex-row md:space-x-8 md:border-0 md:bg-white md:p-0 rtl:space-x-reverse">
<li><a href="#" class="block rounded-sm bg-blue-700 px-3 py-2 text-white md:bg-transparent md:p-0 md:text-blue-700" aria-current="page">Home</a></li>
<li><a href="#" class="block rounded-sm px-3 py-2 text-gray-900 hover:bg-gray-100 md:p-0 md:hover:bg-transparent md:hover:text-blue-700">About</a></li>
</ul>
</div>
</div>
</nav>
{{end}}

View File

@ -0,0 +1,20 @@
{{define "flowbite_tabs"}}
<div class="mb-4 border-b border-gray-200">
<ul class="-mb-px flex flex-wrap text-center text-sm font-medium" id="default-tab" data-tabs-toggle="#default-tab-content" role="tablist">
<li class="me-2" role="presentation">
<button class="inline-block rounded-t-lg border-b-2 p-4" id="profile-tab" data-tabs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
</li>
<li class="me-2" role="presentation">
<button class="inline-block rounded-t-lg border-b-2 p-4" id="dashboard-tab" data-tabs-target="#dashboard" type="button" role="tab" aria-controls="dashboard" aria-selected="false">Dashboard</button>
</li>
</ul>
</div>
<div id="default-tab-content">
<div class="hidden rounded-lg bg-gray-50 p-4" id="profile" role="tabpanel" aria-labelledby="profile-tab">
<p class="text-sm text-gray-500">Profile tab content.</p>
</div>
<div class="hidden rounded-lg bg-gray-50 p-4" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
<p class="text-sm text-gray-500">Dashboard tab content.</p>
</div>
</div>
{{end}}

View File

@ -5,38 +5,85 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}"> <link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<link rel="stylesheet" href="/static/ui/ui.css?v={{.BuildHash}}"> <script src="/static/vendor/htmx.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script src="/static/vendor/flowbite.js"></script>
<script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
</head> </head>
<body> <body class="bg-gray-50 text-gray-900 antialiased">
<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"> <nav class="border-b border-gray-200 bg-white">
<a href="/" class="text-lg font-semibold text-slate-800">Trustcontact</a> <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">
<div class="hidden items-center gap-8 sm:flex"> <span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
{{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> </a>
{{end}}
<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> </div>
</nav> </nav>
<div class="mx-auto my-5 max-w-5xl px-4"> <main class="mx-auto max-w-7xl p-6">
{{template "_flash.html" .}} {{template "_flash.html" .}}
<div class="rounded-xl bg-white p-5 shadow-sm"> {{template "content" .}}
{{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>
</div> </footer>
</body> </body>
</html> </html>

View File

@ -0,0 +1,49 @@
{{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,16 +1,20 @@
{{define "content"}} {{define "content"}}
<div class="space-y-5"> <section class="grid gap-6 md:grid-cols-3">
<div> <article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:col-span-2">
<h1 class="text-2xl font-semibold">Welcome</h1> <h1 class="mb-2 text-2xl font-bold text-gray-900">Dashboard</h1>
{{if .CurrentUser}} {{if .CurrentUser}}
<p class="muted">Bentornato {{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}{{.CurrentUser.Email}}{{end}}.</p> <p class="text-gray-600">Bentornato {{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}{{.CurrentUser.Email}}{{end}}.</p>
{{else}} {{else}}
<p class="muted">Benvenuto.</p> <p class="text-gray-600">Benvenuto.</p>
{{end}} {{end}}
</div> </article>
{{if and .CurrentUser (ne .CurrentUser.Role "admin")}} <article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<p class="muted">Non hai privilegi admin.</p> <h2 class="mb-3 text-lg font-semibold text-gray-900">Quick Links</h2>
{{end}} <div class="space-y-2">
</div> <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>
</div>
</article>
</section>
{{end}} {{end}}

View File

@ -1,6 +1,26 @@
{{define "_flash.html"}}
{{if .FlashSuccess}} {{if .FlashSuccess}}
<div style="background:#dcfce7;color:#166534;padding:12px;border-radius:8px;margin:0 0 12px;">{{.FlashSuccess}}</div> <div class="mb-4 flex items-center rounded-lg border border-green-200 bg-green-50 p-4 text-green-800" role="alert">
<svg class="me-3 inline h-4 w-4 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 0a10 10 0 1 0 10 10A10 10 0 0 0 10 0Zm3.707 8.707-4 4a1 1 0 0 1-1.414 0l-2-2 1.414-1.414L9 10.586l3.293-3.293Z"/>
</svg>
<span class="text-sm font-medium">{{.FlashSuccess}}</span>
</div>
{{end}} {{end}}
{{if .FlashError}} {{if .FlashError}}
<div style="background:#fee2e2;color:#991b1b;padding:12px;border-radius:8px;margin:0 0 12px;">{{.FlashError}}</div> <div class="mb-4 flex items-center rounded-lg border border-red-200 bg-red-50 p-4 text-red-800" role="alert">
<svg class="me-3 inline h-4 w-4 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 0a10 10 0 1 0 10 10A10 10 0 0 0 10 0Zm1 14H9v-2h2Zm0-4H9V5h2Z"/>
</svg>
<span class="text-sm font-medium">{{.FlashError}}</span>
</div>
{{end}}
{{if .FlashWarning}}
<div class="mb-4 flex items-center rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-yellow-800" role="alert">
<svg class="me-3 inline h-4 w-4 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l6.518 11.596c.75 1.334-.213 2.99-1.742 2.99H3.48c-1.53 0-2.492-1.656-1.743-2.99L8.257 3.1ZM11 13H9v2h2v-2Zm0-6H9v5h2V7Z"/>
</svg>
<span class="text-sm font-medium">{{.FlashWarning}}</span>
</div>
{{end}}
{{end}} {{end}}

View File

@ -1,25 +1,19 @@
{{define "content"}} {{define "content"}}
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8"> <div class="flex min-h-screen items-center justify-center">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-amber-100"> <div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<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"> <h1 class="mb-1 text-2xl font-bold text-gray-900">Forgot Password</h1>
<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> <p class="mb-6 text-sm text-gray-500">Inserisci la tua email per ricevere il link di reset.</p>
</svg>
<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>
<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>
<p class="text-center text-sm text-gray-600"><a href="/login" class="text-blue-700 hover:underline">Torna al login</a></p>
</form>
</div> </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> </div>
{{end}} {{end}}

View File

@ -1,10 +1,10 @@
{{define "content"}} {{define "content"}}
<div class="space-y-3"> <section class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
<h1 class="text-2xl font-semibold">Trustcontact</h1> <h1 class="mb-2 text-3xl font-bold text-gray-900">Trustcontact</h1>
<p class="muted">Accedi o registrati per continuare.</p> <p class="mb-6 text-gray-600">Accedi o registrati per continuare.</p>
<div class="row"> <div class="flex flex-wrap gap-3">
<a class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50" href="/login">Accedi</a> <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-slate-300 px-4 py-2 hover:bg-slate-50" href="/signup">Registrati</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> </div>
</div> </section>
{{end}} {{end}}

View File

@ -1,32 +1,27 @@
{{define "content"}} {{define "content"}}
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8"> <div class="flex min-h-screen items-center justify-center">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100"> <div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<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"> <h1 class="mb-1 text-2xl font-bold text-gray-900">Login</h1>
<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> <p class="mb-6 text-sm text-gray-500">Accedi al tuo account.</p>
</svg>
<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>
<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>
<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>
<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>
</div>
</form>
</div> </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> </div>
{{end}} {{end}}

View File

@ -1,24 +1,21 @@
{{define "content"}} {{define "content"}}
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8"> <div class="flex min-h-screen items-center justify-center">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-violet-100"> <div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<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"> <h1 class="mb-1 text-2xl font-bold text-gray-900">Reset Password</h1>
<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> <p class="mb-6 text-sm text-gray-500">Imposta una nuova password.</p>
</svg>
{{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>
<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>
</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>
{{end}}
</div> </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> </div>
{{end}} {{end}}

View File

@ -1,29 +1,24 @@
{{define "content"}} {{define "content"}}
<div class="mx-auto w-full max-w-96 rounded-xl border border-gray-200 px-6 py-8"> <div class="flex min-h-screen items-center justify-center">
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100"> <div class="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm md:p-8">
<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"> <h1 class="mb-1 text-2xl font-bold text-gray-900">Signup</h1>
<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> <p class="mb-6 text-sm text-gray-500">Crea il tuo account.</p>
</svg>
<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>
<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>
<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>
<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>
</form>
</div> </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> </div>
{{end}} {{end}}

View File

@ -1,6 +1,8 @@
{{define "content"}} {{define "content"}}
<h1>Verifica email</h1> <section class="rounded-lg border border-blue-200 bg-blue-50 p-8 shadow-sm" role="status" aria-live="polite">
<p class="muted">Controlla la casella di posta e apri il link di verifica ricevuto.</p> <h1 class="mb-2 text-2xl font-bold text-blue-900">Verifica email</h1>
<p class="muted">Se il link è scaduto, ripeti la registrazione o contatta supporto.</p> <p class="mb-2 text-blue-800">Controlla la casella di posta e apri il link di verifica ricevuto.</p>
<p><a href="/login">Vai al login</a></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>
</section>
{{end}} {{end}}