Compare commits

...

4 Commits

Author SHA1 Message Date
fabio
2dd819444e wc 2026-03-02 19:44:24 +01:00
fabio
25f4701b54 aggiunto il link home 2026-03-01 20:50:02 +01:00
fabio
66a3cc7cdb aggiunto e testato quasar apps 2026-03-01 20:42:27 +01:00
fabio
cdcacadb5f quasar admin setup 2026-03-01 17:51:00 +01:00
98 changed files with 2554 additions and 1016 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: tw-build tw-watch htmx-copy flowbite-copy assets server test db-reset fmt .PHONY: tw-build tw-watch htmx-copy flags-copy assets server test db-reset fmt
tw-build: tw-build:
npm run tw:build npm run tw:build
@@ -9,10 +9,10 @@ tw-watch:
htmx-copy: htmx-copy:
mkdir -p web/static/vendor && cp node_modules/htmx.org/dist/htmx.min.js web/static/vendor/htmx.min.js mkdir -p web/static/vendor && cp node_modules/htmx.org/dist/htmx.min.js web/static/vendor/htmx.min.js
flowbite-copy: flags-copy:
mkdir -p web/static/vendor && cp node_modules/flowbite/dist/flowbite.min.js web/static/vendor/flowbite.js mkdir -p web/static/vendor/flags && cp assets/flags/*.svg web/static/vendor/flags/
assets: htmx-copy flowbite-copy tw-build assets: htmx-copy flags-copy tw-build
server: server:
go run ./cmd/server go run ./cmd/server

View File

@@ -18,6 +18,14 @@ Terminale 2:
make server make server
``` ```
Admin SPA (Quasar):
- il backend serve `quasar/admin_section/dist/spa` sotto `/admin` (protetto da auth + ruolo admin)
- build frontend admin: `cd quasar/admin_section && npm i && npm run build`
Private SPA (Quasar):
- il backend serve `quasar/private_section/dist/spa` sotto `/private` (protetto da auth)
- build frontend private: `cd quasar/private_section && npm i && npm run build`
`make assets` esegue: `make assets` esegue:
- copia di `node_modules/flowbite/dist/flowbite.min.js` in `web/static/vendor/flowbite.js` - copia di `node_modules/flowbite/dist/flowbite.min.js` in `web/static/vendor/flowbite.js`
- build Tailwind in `web/static/css/app.css` - build Tailwind in `web/static/css/app.css`
@@ -65,8 +73,8 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
## Template Directories ## Template Directories
- Public: `web/templates/public` - Public: `web/templates/public`
- Private: `web/templates/private` - Private: `quasar/private_section/dist/spa` (SPA servita da Go sotto `/private`)
- Admin: `web/templates/admin` - Admin: `quasar/admin_section/dist/spa` (SPA servita da Go sotto `/admin`)
## Email in Develop ## Email in Develop

5
assets/flags/ch.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Swiss flag">
<rect width="32" height="32" fill="#d52b1e"/>
<rect x="13" y="6" width="6" height="20" fill="#ffffff"/>
<rect x="6" y="13" width="20" height="6" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

5
assets/flags/de.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="German flag">
<rect width="48" height="10.67" x="0" y="0" fill="#000000"/>
<rect width="48" height="10.67" x="0" y="10.67" fill="#dd0000"/>
<rect width="48" height="10.66" x="0" y="21.34" fill="#ffce00"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

9
assets/flags/en.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="English flag">
<rect width="48" height="32" fill="#012169"/>
<path d="M0 0 48 32M48 0 0 32" stroke="#ffffff" stroke-width="6"/>
<path d="M0 0 48 32M48 0 0 32" stroke="#c8102e" stroke-width="3"/>
<rect x="20" y="0" width="8" height="32" fill="#ffffff"/>
<rect x="0" y="12" width="48" height="8" fill="#ffffff"/>
<rect x="22" y="0" width="4" height="32" fill="#c8102e"/>
<rect x="0" y="14" width="48" height="4" fill="#c8102e"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

53
assets/flags/en_us.svg Normal file
View File

@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="United States flag">
<rect width="48" height="32" fill="#b22234"/>
<g fill="#ffffff">
<rect y="2.46" width="48" height="2.46"/>
<rect y="7.38" width="48" height="2.46"/>
<rect y="12.30" width="48" height="2.46"/>
<rect y="17.22" width="48" height="2.46"/>
<rect y="22.14" width="48" height="2.46"/>
<rect y="27.06" width="48" height="2.46"/>
</g>
<rect width="20" height="17.23" fill="#3c3b6e"/>
<g fill="#ffffff">
<circle cx="2.2" cy="2.2" r="0.7"/>
<circle cx="5.4" cy="2.2" r="0.7"/>
<circle cx="8.6" cy="2.2" r="0.7"/>
<circle cx="11.8" cy="2.2" r="0.7"/>
<circle cx="15.0" cy="2.2" r="0.7"/>
<circle cx="18.2" cy="2.2" r="0.7"/>
<circle cx="3.8" cy="4.4" r="0.7"/>
<circle cx="7.0" cy="4.4" r="0.7"/>
<circle cx="10.2" cy="4.4" r="0.7"/>
<circle cx="13.4" cy="4.4" r="0.7"/>
<circle cx="16.6" cy="4.4" r="0.7"/>
<circle cx="2.2" cy="6.6" r="0.7"/>
<circle cx="5.4" cy="6.6" r="0.7"/>
<circle cx="8.6" cy="6.6" r="0.7"/>
<circle cx="11.8" cy="6.6" r="0.7"/>
<circle cx="15.0" cy="6.6" r="0.7"/>
<circle cx="18.2" cy="6.6" r="0.7"/>
<circle cx="3.8" cy="8.8" r="0.7"/>
<circle cx="7.0" cy="8.8" r="0.7"/>
<circle cx="10.2" cy="8.8" r="0.7"/>
<circle cx="13.4" cy="8.8" r="0.7"/>
<circle cx="16.6" cy="8.8" r="0.7"/>
<circle cx="2.2" cy="11" r="0.7"/>
<circle cx="5.4" cy="11" r="0.7"/>
<circle cx="8.6" cy="11" r="0.7"/>
<circle cx="11.8" cy="11" r="0.7"/>
<circle cx="15.0" cy="11" r="0.7"/>
<circle cx="18.2" cy="11" r="0.7"/>
<circle cx="3.8" cy="13.2" r="0.7"/>
<circle cx="7.0" cy="13.2" r="0.7"/>
<circle cx="10.2" cy="13.2" r="0.7"/>
<circle cx="13.4" cy="13.2" r="0.7"/>
<circle cx="16.6" cy="13.2" r="0.7"/>
<circle cx="2.2" cy="15.4" r="0.7"/>
<circle cx="5.4" cy="15.4" r="0.7"/>
<circle cx="8.6" cy="15.4" r="0.7"/>
<circle cx="11.8" cy="15.4" r="0.7"/>
<circle cx="15.0" cy="15.4" r="0.7"/>
<circle cx="18.2" cy="15.4" r="0.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

5
assets/flags/fr.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="French flag">
<rect width="16" height="32" x="0" y="0" fill="#0055a4"/>
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
<rect width="16" height="32" x="32" y="0" fill="#ef4135"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

5
assets/flags/it.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Italian flag">
<rect width="16" height="32" x="0" y="0" fill="#009246"/>
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
<rect width="16" height="32" x="32" y="0" fill="#ce2b37"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@layer utilities { @layer utilities {
.flag-lang { .flag-lang {

View File

@@ -1,71 +0,0 @@
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

@@ -1,79 +0,0 @@
TASK: Aggiungere Dark Mode globale con toggle nel footer (Flowbite + Tailwind).
Vincoli:
- Usare Tailwind dark mode con strategia "class" (non media)
- Toggle nel footer visibile su tutte le pagine (layout globale)
- Stato persistente con localStorage
- Default: se non cè preferenza salvata, usare prefers-color-scheme
- Nessun framework JS aggiuntivo
- Non rompere HTMX e Flowbite
- Accessibile (aria-label, stato, focus)
1) Tailwind config
- Aggiornare tailwind.config.js:
- impostare `darkMode: 'class'`
- assicurarsi che content includa templates e node_modules/flowbite come già configurato
2) JS globale
- Creare file /web/static/vendor/theme.js (o /web/static/js/theme.js se preferisci) con:
- allavvio (prima del render visibile):
- leggere localStorage key `theme` ('dark'|'light')
- se non presente, leggere `window.matchMedia('(prefers-color-scheme: dark)')`
- applicare/rimuovere classe `dark` su <html> (document.documentElement)
- esporre funzione toggleTheme() che:
- toggla classe `dark`
- salva preferenza in localStorage
- aggiorna label testo del bottone e aria-pressed
- gestire aggiornamento se lutente non ha preferenza salvata e cambia prefers-color-scheme (listener matchMedia), opzionale ma gradito
3) Includere theme.js nel layout
- In /web/templates/layout.html:
- includere theme.js nel <head> prima del CSS per evitare FOUC:
<script src="/static/vendor/theme.js"></script>
- poi link CSS e script htmx/flowbite come già presenti
- aggiungere classi base al body per dark:
- bg-white dark:bg-gray-900
- text-gray-900 dark:text-gray-100
- min-h-screen flex flex-col
- contenuto in main con flex-1
4) Footer + toggle button (Flowbite style)
- Nel footer del layout aggiungere un bottone:
- posizionato a destra (o center se preferisci) con icona (testo va bene senza icone)
- classi Flowbite/Tailwind:
- px-3 py-2 text-sm font-medium rounded-lg
- bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700
- attributi accessibilità:
- id="themeToggle"
- type="button"
- aria-label="Toggle dark mode"
- aria-pressed="false" (aggiornato via JS)
- testo dinamico: "Dark mode" / "Light mode" o "Tema: Scuro/Chiaro"
- Aggiungere onclick:
- onclick="window.toggleTheme && window.toggleTheme()"
5) Aggiornare componenti base per dark
- Aggiornare /web/templates/components/navbar.html (se esiste) e altri partial principali con classi dark:
- Navbar: bg-white dark:bg-gray-900, border-gray-200 dark:border-gray-700
- Dropdown: bg-white dark:bg-gray-800, text colors
- Cards: bg-white dark:bg-gray-800
- Tables: dark divide colors
Non serve perfezione totale, ma assicurare leggibilità.
6) README
- Aggiornare README con nota:
- dark mode persistente in localStorage key "theme"
7) Criteri di accettazione
- Il toggle è visibile nel footer su tutte le pagine
- Il tema persiste al reload
- Default segue prefers-color-scheme se non impostato manualmente
- Nessun flash di tema sbagliato (FOUC minimizzato)
- Non rompe Flowbite JS, modals, dropdown e HTMX
- Accessibile: bottone focusable e aria-pressed aggiornato
Esegui:
- make tw-build (o make tw-watch per verificare)
- Avvia server e verifica cambio tema su /login e /users.
Correggi eventuali classi mancanti.

View File

@@ -1,141 +0,0 @@
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.

View File

@@ -1,13 +0,0 @@
Sei Codex in VS Code. Lavora direttamente nel workspace.
Obiettivo: creare un boilerplate riusabile “GoFiber MVC + HTMX + Svelte Custom Elements UI kit + GORM + SQLite/Postgres + Auth + Email sink + CORS + template directory public/private/admin + role admin”.
1) Scansiona il workspace e dimmi cosa esiste già.
2) Crea/aggiorna la struttura cartelle secondo questa convenzione:
/cmd/server
/internal/{app,config,http,middleware,db,models,repo,services,controllers,auth,mailer}
/web/{templates/{public,private,admin},emails/templates,static/{vendor,ui,css}}
/ui-kit
/data (solo dev)
3) Crea una TODO checklist in README.md con i passi rimanenti.
Non implementare ancora logica: solo struttura + README e .gitignore.

View File

@@ -1,24 +0,0 @@
Implementa internal/config e internal/app.
- Aggiungi internal/config/config.go:
- carica .env se presente (godotenv)
- espone Config con: AppName, Env (develop|prod), Port, BaseURL, BuildHash
DBDriver (sqlite|postgres), SQLitePath, PostgresDSN
CORS settings (origins/headers/methods/credentials)
SessionKey
SMTP settings + EmailSinkDir
Flags: AutoMigrate, SeedEnabled
- valida i campi essenziali (es. DB DSN se postgres)
- Aggiungi internal/app/app.go:
- crea fiber.App
- registra CORS middleware
- registra session store
- init DB (internal/db) + migrate/seed (in base ai flag)
- registra router (internal/http/router.go)
- espone NewApp(cfg) (*fiber.App, error)
- Aggiorna cmd/server/main.go per usare internal/app.
Crea/aggiorna .env.example e .gitignore (escludi .env, /data, db sqlite, email sink).
Scrivi codice compilabile.

View File

@@ -1,20 +0,0 @@
Aggiungi DX boilerplate.
- Makefile:
- make dev (go run ./cmd/server)
- make ui-build (cd ui-kit && npm i && npm run build)
- make ui-dev (watch)
- make test (go test ./...)
- make db-reset (solo sqlite: rimuovi ./data/app.db)
- make fmt (gofmt)
- docker-compose.yml:
- postgres service (porta 5432)
- env compatibile con DB_PG_DSN
- README.md:
- Quickstart sqlite
- Quickstart postgres (docker compose)
- dove stanno templates public/private/admin
- email in develop: ./data/emails
- build UI kit

View File

@@ -1,19 +0,0 @@
Implementa internal/db (db.go, migrate.go, seed.go) con GORM.
- db.go: Open(cfg) (*gorm.DB, error) con switch sqlite/postgres.
- sqlite usa cfg.SQLitePath, crea directory se serve.
- postgres usa cfg.PostgresDSN.
- logger più verboso in develop.
- migrate.go: Migrate(db) che fa AutoMigrate su tutti i modelli (User, EmailVerificationToken, PasswordResetToken).
- esegui solo se cfg.AutoMigrate=true (gestisci in app.go o in migrate.go).
- seed.go: Seed(db) idempotente se cfg.SeedEnabled=true:
- in develop crea:
- admin@example.com (role=admin, verified=true, password="password")
- user@example.com (role=user, verified=true, password="password")
- crea anche utenti demo aggiuntivi per tabella.
- usa upsert by email (GORM clauses OnConflict dove possibile).
- NON loggare password in chiaro.
Aggiorna/crea internal/models con i modelli necessari.

View File

@@ -1,19 +0,0 @@
Implementa internal/models e internal/auth.
- internal/models/user.go:
- User: ID, Email unique, PasswordHash, EmailVerified, Role (default user), timestamps.
- internal/models/auth_tokens.go:
- EmailVerificationToken: UserID, TokenHash unique, ExpiresAt, timestamps
- PasswordResetToken: UserID, TokenHash unique, ExpiresAt, timestamps
- internal/auth/passwords.go:
- HashPassword(plain) -> hash (bcrypt)
- ComparePassword(hash, plain) -> bool/error
- internal/auth/tokens.go:
- NewToken() -> plainToken (base64url random 32+ bytes)
- HashToken(plainToken) -> hex/bytes SHA-256 string
- ExpiresAt helpers (verify 24h, reset 1h)
Assicurati che nel DB venga salvato SOLO lhash del token.

View File

@@ -1,24 +0,0 @@
Implementa internal/mailer.
Requisiti:
- directory template email: /web/emails/templates
- verify_email.html + .txt
- reset_password.html + .txt
- internal/mailer/templates.go:
- carica e renderizza template (html+txt) con dati: AppName, BaseURL, VerifyURL/ResetURL, UserEmail.
- internal/mailer/mailer.go:
- interfaccia Mailer { Send(ctx, to, subject, htmlBody, textBody) error }
- factory NewMailer(cfg) che ritorna:
- sink mailer se cfg.Env==develop
- smtp mailer altrimenti
- internal/mailer/sink.go:
- salva in cfg.EmailSinkDir file con timestamp__type__to.eml (o .txt/.html)
- includi subject, to, bodies e link.
- internal/mailer/smtp.go:
- invio via SMTP usando cfg.SMTPHost/Port/User/Password/From/FromName.
Aggiorna README con “in develop le email sono salvate in ./data/emails”.

View File

@@ -1,14 +0,0 @@
Aggiungi session e middleware.
- Usa Fiber session middleware (cookie session). Configura key da cfg.SessionKey, cookie secure in prod, SameSite Lax, HttpOnly.
- Implementa internal/http/middleware:
- RequireAuth: se non loggato redirect /login
- RequireAdmin: se role != admin -> 403 (pagina admin/forbidden o testo)
- CurrentUser helper (legge user_id da sessione, carica user da DB con repo)
- Implementa flash messages (success/error) in sessione:
- SetFlashSuccess/SetFlashError
- ConsumeFlash middleware che aggiunge al template data
Aggiorna layout.html per mostrare flash e navbar diversa per public/private/admin.

View File

@@ -1,27 +0,0 @@
Implementa AUTH completo (server-rendered) e templates in /web/templates/public.
Routes:
- GET/POST /signup
- GET/POST /login
- POST /logout
- GET /verify-email?token=...
- GET/POST /forgot-password
- GET/POST /reset-password?token=...
Comportamento:
- Signup crea user (role=user, verified=false), genera verify token (hash in DB), invia email (mailer).
- Login: blocca se email non verificata.
- Verify-email: valida token, set EmailVerified=true, elimina token.
- Forgot-password: risposta sempre generica; se user esiste+verified, genera reset token e invia email.
- Reset-password: valida token, aggiorna password, elimina token.
Crea templates:
- public/login.html
- public/signup.html
- public/forgot_password.html
- public/reset_password.html
- public/verify_notice.html
- public/home.html (opzionale)
Aggiungi partial per flash (public/_flash.html) e includilo nel layout.
Usa repo/service per accesso DB e logica (non tutto nel controller).

View File

@@ -1,22 +0,0 @@
Implementa modulo “users” sotto /web/templates/private/users.
Routes protette (RequireAuth):
- GET /users -> pagina con search + container tabella
- GET /users/table -> partial HTML tabella (htmx)
- GET /users/:id/modal -> partial HTML contenuto modal
Requisiti tabella:
- query params: q, sort (id|name|email whitelist), dir (asc|desc), page, pageSize
- server-driven paging/sort/search usando GORM (Count + Limit/Offset + Order)
- _table.html deve includere:
- header th cliccabili con hx-get (toggle dir)
- pager prev/next con hx-get
- bottone “Apri” che hx-get sul modal e hx-target="#userModal" hx-swap="innerHTML"
- apri modal via JS minimal: setAttribute('open','') dopo swap (o onclick)
Crea template:
- private/users/index.html
- private/users/_table.html
- private/users/_modal.html
Integra <ui-modal id="userModal"> nella index privata.

View File

@@ -1,11 +0,0 @@
Implementa area admin.
Routes protette (RequireAuth + RequireAdmin):
- GET /admin -> admin/dashboard.html
- GET /admin/users -> pagina elenco utenti (server-rendered semplice)
Templates:
- admin/dashboard.html
- admin/users.html
Navbar nel layout deve mostrare link Admin solo se role=admin.

View File

@@ -1,33 +0,0 @@
Crea /ui-kit come progetto Vite + Svelte per custom elements.
Requisiti:
- build deve scrivere direttamente in ../web/static/ui:
- ui.esm.js
- ui.css (tokens+base)
- src/index.ts registra:
- ui-modal
- ui-drop-down
- ui-data-table-shell (driver htmx per aggiornare un target)
Componenti:
1) UiModal.svelte:
- <svelte:options customElement="ui-modal" />
- attributi: title, open (boolean presence)
- close on ESC, backdrop click
- focus trap minimale
- emette evento "ui:close" (bubbles+composed)
- slot contenuto (HTMX swappa dentro al tag)
2) UiDropDown.svelte:
- usa <option> del light DOM
- espone value/name/placeholder/disabled
- integra con form MVC (hidden input name=...)
- emette change + ui:change
3) UiDataTableShell.svelte:
- attributi: endpoint, target, page-size
- input search -> usa htmx.ajax('GET', url, {target}) se disponibile
- non renderizza tabella, solo toolbar
Aggiorna layout per includere /static/ui/ui.css e /static/ui/ui.esm.js con ?v={{.BuildHash}}.
Aggiorna README con comandi npm.

View File

@@ -1,49 +1,27 @@
package controllers package controllers
import ( import (
"bytes" "path/filepath"
"html/template"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type AdminController struct{} type AdminController struct {
spaDir string
}
func NewAdminController() *AdminController { func NewAdminController(spaDir string) *AdminController {
return &AdminController{} return &AdminController{spaDir: spaDir}
} }
func (ac *AdminController) Dashboard(c *fiber.Ctx) error { func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
return renderAdminPage(c, "Admin") return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
} }
func renderAdminPage(c *fiber.Ctx, title string) error { func (ac *AdminController) Fallback(c *fiber.Ctx) error {
viewData := map[string]any{ return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
"Title": title, }
"NavSection": "admin",
} func (ac *AdminController) Favicon(c *fiber.Ctx) error {
for k, v := range localsTemplateData(c) { return c.SendFile(filepath.Join(ac.spaDir, "favicon.ico"))
viewData[k] = v
}
files := []string{
"web/templates/layout.html",
"web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/public/_flash.html",
"web/templates/admin/admin.html",
}
tmpl, err := template.ParseFiles(files...)
if err != nil {
return err
}
var out bytes.Buffer
if err := tmpl.ExecuteTemplate(&out, "layout.html", viewData); err != nil {
return err
}
c.Type("html", "utf-8")
return c.Send(out.Bytes())
} }

View File

@@ -25,13 +25,6 @@ func (ac *AuthController) ShowHome(c *fiber.Ctx) error {
}) })
} }
func (ac *AuthController) ShowWelcome(c *fiber.Ctx) error {
return renderPrivate(c, "welcome.html", map[string]any{
"Title": "Welcome",
"NavSection": "private",
})
}
func (ac *AuthController) ShowSignup(c *fiber.Ctx) error { func (ac *AuthController) ShowSignup(c *fiber.Ctx) error {
return renderPublic(c, "signup.html", map[string]any{ return renderPublic(c, "signup.html", map[string]any{
"Title": "Sign up", "Title": "Sign up",
@@ -97,7 +90,7 @@ func (ac *AuthController) Login(c *fiber.Ctx) error {
if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil { if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil {
return err return err
} }
return c.Redirect("/welcome") return c.Redirect("/private")
} }
func (ac *AuthController) Logout(c *fiber.Ctx) error { func (ac *AuthController) Logout(c *fiber.Ctx) error {

View File

@@ -0,0 +1,27 @@
package controllers
import (
"path/filepath"
"github.com/gofiber/fiber/v2"
)
type PrivateController struct {
spaDir string
}
func NewPrivateController(spaDir string) *PrivateController {
return &PrivateController{spaDir: spaDir}
}
func (ac *PrivateController) Dashboard(c *fiber.Ctx) error {
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
}
func (ac *PrivateController) Fallback(c *fiber.Ctx) error {
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
}
func (ac *PrivateController) Favicon(c *fiber.Ctx) error {
return c.SendFile(filepath.Join(ac.spaDir, "favicon.ico"))
}

View File

@@ -1,27 +0,0 @@
package controllers
import (
"trustcontact/internal/services"
"github.com/gofiber/fiber/v2"
)
type UsersController struct {
usersService *services.UsersService
}
func NewUsersController(usersService *services.UsersService) *UsersController {
return &UsersController{usersService: usersService}
}
func (uc *UsersController) Index(c *fiber.Ctx) error {
return renderAdminPage(c, "Admin")
}
func (uc *UsersController) Table(c *fiber.Ctx) error {
return renderAdminPage(c, "Admin")
}
func (uc *UsersController) Modal(c *fiber.Ctx) error {
return renderAdminPage(c, "Admin")
}

View File

@@ -2,6 +2,7 @@ package http
import ( import (
"fmt" "fmt"
"path/filepath"
"trustcontact/internal/config" "trustcontact/internal/config"
"trustcontact/internal/controllers" "trustcontact/internal/controllers"
httpmw "trustcontact/internal/http/middleware" httpmw "trustcontact/internal/http/middleware"
@@ -28,9 +29,10 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
return fmt.Errorf("init auth service: %w", err) return fmt.Errorf("init auth service: %w", err)
} }
authController := controllers.NewAuthController(authService) authController := controllers.NewAuthController(authService)
usersService := services.NewUsersService(database) privateSPADir := filepath.FromSlash("quasar/private_section/dist/spa")
usersController := controllers.NewUsersController(usersService) privateController := controllers.NewPrivateController(privateSPADir)
adminController := controllers.NewAdminController() adminSPADir := filepath.FromSlash("quasar/admin_section/dist/spa")
adminController := controllers.NewAdminController(adminSPADir)
app.Get("/healthz", func(c *fiber.Ctx) error { app.Get("/healthz", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
@@ -49,20 +51,32 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
app.Get("/reset-password", authController.ShowResetPassword) app.Get("/reset-password", authController.ShowResetPassword)
app.Post("/reset-password", authController.ResetPassword) app.Post("/reset-password", authController.ResetPassword)
app.Get("/forbidden", authController.ShowForbidden) app.Get("/forbidden", authController.ShowForbidden)
app.Get("/welcome", httpmw.RequireAuth(), authController.ShowWelcome)
app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage) app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage)
app.Post("/preferences/theme", httpmw.RequireAuth(), authController.UpdateTheme) app.Post("/preferences/theme", httpmw.RequireAuth(), authController.UpdateTheme)
// Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
// Protect them with the same auth/admin middleware used by /admin.
app.Use("/assets", httpmw.RequireAuth(), httpmw.RequireAdmin())
app.Use("/icons", httpmw.RequireAuth(), httpmw.RequireAdmin())
app.Get("/favicon.ico", httpmw.RequireAuth(), httpmw.RequireAdmin(), privateController.Favicon)
app.Static("/assets", filepath.Join(privateSPADir, "assets"))
app.Static("/icons", filepath.Join(privateSPADir, "icons"))
private := app.Group("/private", httpmw.RequireAuth(), httpmw.RequireAdmin()) private := app.Group("/private", httpmw.RequireAuth(), httpmw.RequireAdmin())
private.Get("/", func(c *fiber.Ctx) error { private.Get("/", privateController.Dashboard)
return c.Redirect("/admin/users") private.Get("/*", privateController.Fallback)
})
// Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
// Protect them with the same auth/admin middleware used by /admin.
app.Use("/assets", httpmw.RequireAuth(), httpmw.RequireAdmin())
app.Use("/icons", httpmw.RequireAuth(), httpmw.RequireAdmin())
app.Get("/favicon.ico", httpmw.RequireAuth(), httpmw.RequireAdmin(), adminController.Favicon)
app.Static("/assets", filepath.Join(adminSPADir, "assets"))
app.Static("/icons", filepath.Join(adminSPADir, "icons"))
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin()) admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
admin.Get("/", adminController.Dashboard) admin.Get("/", adminController.Dashboard)
admin.Get("/users", usersController.Index) admin.Get("/*", adminController.Fallback)
admin.Get("/users/table", usersController.Table)
admin.Get("/users/:id/modal", usersController.Modal)
return nil return nil
} }

1063
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
}, },
"dependencies": { "dependencies": {
"flowbite": "^3.1.2",
"htmx.org": "^2.0.6" "htmx.org": "^2.0.6"
} }
} }

View File

@@ -3,6 +3,10 @@
import { defineConfig } from '#q-app/wrappers'; import { defineConfig } from '#q-app/wrappers';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import dotenv from 'dotenv';
dotenv.config({ path: resolve(__dirname, '../../.env') });
export default defineConfig((ctx) => { export default defineConfig((ctx) => {
return { return {
@@ -33,6 +37,9 @@ export default defineConfig((ctx) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
build: { build: {
env: {
SITE_URL: process.env.SITE_URL || '',
},
target: { target: {
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'], browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
node: 'node20', node: 'node20',

View File

@@ -1,27 +0,0 @@
<template>
<q-item clickable tag="a" target="_blank" :href="link">
<q-item-section v-if="icon" avatar>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
export interface EssentialLinkProps {
title: string;
caption?: string;
link?: string;
icon?: string;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',
link: '#',
icon: '',
});
</script>

View File

@@ -1,37 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { Todo, Meta } from './models';
interface Props {
title: string;
todos?: Todo[];
meta: Meta;
active: boolean;
}
const props = withDefaults(defineProps<Props>(), {
todos: () => [],
});
const clickCount = ref(0);
function increment() {
clickCount.value += 1;
return clickCount.value;
}
const todoCount = computed(() => props.todos.length);
</script>

View File

@@ -1,8 +0,0 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}

View File

@@ -1,6 +1,7 @@
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
NODE_ENV: string; NODE_ENV: string;
SITE_URL: string | undefined;
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
VUE_ROUTER_BASE: string | undefined; VUE_ROUTER_BASE: string | undefined;
} }

View File

@@ -12,9 +12,14 @@
<q-drawer v-model="leftDrawerOpen" show-if-above bordered> <q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<q-list> <q-list>
<q-item-label header> Essential Links </q-item-label> <q-item-label header> List Items </q-item-label>
<q-item
<EssentialLink v-for="link in linksList" :key="link.title" v-bind="link" /> clickable
tag="a"
:href="privateLink"
>
<q-item-section>Private</q-item-section>
</q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
@@ -26,54 +31,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import EssentialLink, { type EssentialLinkProps } from 'components/EssentialLink.vue';
const linksList: EssentialLinkProps[] = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev',
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework',
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev',
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev',
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev',
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev',
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev',
},
];
const leftDrawerOpen = ref(false); const leftDrawerOpen = ref(false);
const siteUrl = (process.env.SITE_URL || '').replace(/\/+$/, '');
const privateLink = `${siteUrl}/private`;
function toggleLeftDrawer() { function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value; leftDrawerOpen.value = !leftDrawerOpen.value;

View File

@@ -1,43 +1,9 @@
<template> <template>
<q-page class="row items-center justify-evenly"> <q-page class="row items-center justify-evenly">
<example-component admin page
title="Example component"
active
:todos="todos"
:meta="meta"
></example-component>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import type { Todo, Meta } from 'components/models';
import ExampleComponent from 'components/ExampleComponent.vue';
const todos = ref<Todo[]>([
{
id: 1,
content: 'ct1',
},
{
id: 2,
content: 'ct2',
},
{
id: 3,
content: 'ct3',
},
{
id: 4,
content: 'ct4',
},
{
id: 5,
content: 'ct5',
},
]);
const meta = ref<Meta>({
totalCount: 1200,
});
</script> </script>

View File

@@ -1,3 +1,7 @@
{ {
"extends": "./.quasar/tsconfig.json" "extends": "./.quasar/tsconfig.json",
"compilerOptions": {
"skipLibCheck": true
},
"exclude": ["node_modules", "dist"]
} }

View File

@@ -15,29 +15,29 @@
"postinstall": "quasar prepare" "postinstall": "quasar prepare"
}, },
"dependencies": { "dependencies": {
"vue-i18n": "^11.0.0", "@quasar/extras": "^1.17.0",
"pinia": "^3.0.1", "pinia": "^3.0.4",
"@quasar/extras": "^1.16.4", "quasar": "^2.18.6",
"quasar": "^2.16.0", "vue": "^3.5.29",
"vue": "^3.5.22", "vue-i18n": "^11.2.8",
"vue-router": "^5.0.0" "vue-router": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.39.3",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^10.4.0",
"globals": "^16.4.0",
"vue-tsc": "^3.0.7",
"@vue/eslint-config-typescript": "^14.4.0",
"vite-plugin-checker": "^0.11.0",
"vue-eslint-parser": "^10.2.0",
"@vue/eslint-config-prettier": "^10.1.0",
"prettier": "^3.3.3",
"@types/node": "^20.5.9",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@quasar/app-vite": "^2.1.0", "@quasar/app-vite": "^2.4.1",
"autoprefixer": "^10.4.2", "@types/node": "^20.19.35",
"typescript": "^5.9.2" "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.3",
"eslint-plugin-vue": "^10.8.0",
"globals": "^16.5.0",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"vite-plugin-checker": "^0.11.0",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5"
}, },
"engines": { "engines": {
"node": "^28 || ^26 || ^24 || ^22 || ^20", "node": "^28 || ^26 || ^24 || ^22 || ^20",

View File

@@ -9,68 +9,68 @@ importers:
.: .:
dependencies: dependencies:
'@quasar/extras': '@quasar/extras':
specifier: ^1.16.4 specifier: ^1.17.0
version: 1.17.0 version: 1.17.0
pinia: pinia:
specifier: ^3.0.1 specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))
quasar: quasar:
specifier: ^2.16.0 specifier: ^2.18.6
version: 2.18.6 version: 2.18.6
vue: vue:
specifier: ^3.5.22 specifier: ^3.5.29
version: 3.5.29(typescript@5.9.3) version: 3.5.29(typescript@5.9.3)
vue-i18n: vue-i18n:
specifier: ^11.0.0 specifier: ^11.2.8
version: 11.2.8(vue@3.5.29(typescript@5.9.3)) version: 11.2.8(vue@3.5.29(typescript@5.9.3))
vue-router: vue-router:
specifier: ^5.0.0 specifier: ^5.0.3
version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^9.14.0 specifier: ^9.39.3
version: 9.39.3 version: 9.39.3
'@intlify/unplugin-vue-i18n': '@intlify/unplugin-vue-i18n':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(rollup@4.59.0)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3))) version: 4.0.0(rollup@4.59.0)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))
'@quasar/app-vite': '@quasar/app-vite':
specifier: ^2.1.0 specifier: ^2.4.1
version: 2.4.1(@types/node@20.19.35)(eslint@9.39.3)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(quasar@2.18.6)(rollup@4.59.0)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)(vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yaml@2.8.2) version: 2.4.1(@types/node@20.19.35)(eslint@9.39.3)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(quasar@2.18.6)(rollup@4.59.0)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)(vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yaml@2.8.2)
'@types/node': '@types/node':
specifier: ^20.5.9 specifier: ^20.19.35
version: 20.19.35 version: 20.19.35
'@vue/eslint-config-prettier': '@vue/eslint-config-prettier':
specifier: ^10.1.0 specifier: ^10.2.0
version: 10.2.0(eslint@9.39.3)(prettier@3.8.1) version: 10.2.0(eslint@9.39.3)(prettier@3.8.1)
'@vue/eslint-config-typescript': '@vue/eslint-config-typescript':
specifier: ^14.4.0 specifier: ^14.7.0
version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3)))(eslint@9.39.3)(typescript@5.9.3) version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3)))(eslint@9.39.3)(typescript@5.9.3)
autoprefixer: autoprefixer:
specifier: ^10.4.2 specifier: ^10.4.27
version: 10.4.27(postcss@8.5.6) version: 10.4.27(postcss@8.5.6)
eslint: eslint:
specifier: ^9.14.0 specifier: ^9.39.3
version: 9.39.3 version: 9.39.3
eslint-plugin-vue: eslint-plugin-vue:
specifier: ^10.4.0 specifier: ^10.8.0
version: 10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3)) version: 10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3))
globals: globals:
specifier: ^16.4.0 specifier: ^16.5.0
version: 16.5.0 version: 16.5.0
prettier: prettier:
specifier: ^3.3.3 specifier: ^3.8.1
version: 3.8.1 version: 3.8.1
typescript: typescript:
specifier: ^5.9.2 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
vite-plugin-checker: vite-plugin-checker:
specifier: ^0.11.0 specifier: ^0.11.0
version: 0.11.0(eslint@9.39.3)(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@20.19.35)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3)) version: 0.11.0(eslint@9.39.3)(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@20.19.35)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))
vue-eslint-parser: vue-eslint-parser:
specifier: ^10.2.0 specifier: ^10.4.0
version: 10.4.0(eslint@9.39.3) version: 10.4.0(eslint@9.39.3)
vue-tsc: vue-tsc:
specifier: ^3.0.7 specifier: ^3.2.5
version: 3.2.5(typescript@5.9.3) version: 3.2.5(typescript@5.9.3)
packages: packages:

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 859 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -3,6 +3,10 @@
import { defineConfig } from '#q-app/wrappers'; import { defineConfig } from '#q-app/wrappers';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import dotenv from 'dotenv';
dotenv.config({ path: resolve(__dirname, '../../.env') });
export default defineConfig((ctx) => { export default defineConfig((ctx) => {
return { return {
@@ -33,6 +37,9 @@ export default defineConfig((ctx) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
build: { build: {
env: {
SITE_URL: process.env.SITE_URL || '',
},
target: { target: {
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'], browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
node: 'node20', node: 'node20',

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,6 +1,7 @@
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
NODE_ENV: string; NODE_ENV: string;
SITE_URL: string | undefined;
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
VUE_ROUTER_BASE: string | undefined; VUE_ROUTER_BASE: string | undefined;
} }

View File

@@ -0,0 +1,60 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
<q-toolbar-title> Quasar App </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
</q-header>
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<q-list>
<q-item-label header> items list </q-item-label>
<q-item
clickable
tag="a"
:href="homeLink"
>
<q-item-section>Home</q-item-section>
</q-item>
<q-item
clickable
tag="a"
:href="privateLink"
>
<q-item-section>Private</q-item-section>
</q-item>
<q-item
clickable
tag="a"
:href="adminLink"
>
<q-item-section>Admin</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const leftDrawerOpen = ref(false);
const siteUrl = (process.env.SITE_URL || '').replace(/\/+$/, '');
const privateLink = `${siteUrl}/private`;
const adminLink = `${siteUrl}/admin`;
const homeLink = `${siteUrl}/`;
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<q-page class="row items-center justify-evenly">
private page
</q-page>
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,7 @@
{
"extends": "./.quasar/tsconfig.json",
"compilerOptions": {
"skipLibCheck": true
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,27 +0,0 @@
<template>
<q-item clickable tag="a" target="_blank" :href="link">
<q-item-section v-if="icon" avatar>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
export interface EssentialLinkProps {
title: string;
caption?: string;
link?: string;
icon?: string;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',
link: '#',
icon: '',
});
</script>

View File

@@ -1,37 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { Todo, Meta } from './models';
interface Props {
title: string;
todos?: Todo[];
meta: Meta;
active: boolean;
}
const props = withDefaults(defineProps<Props>(), {
todos: () => [],
});
const clickCount = ref(0);
function increment() {
clickCount.value += 1;
return clickCount.value;
}
const todoCount = computed(() => props.todos.length);
</script>

View File

@@ -1,8 +0,0 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}

View File

@@ -1,81 +0,0 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
<q-toolbar-title> Quasar App </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
</q-header>
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<q-list>
<q-item-label header> Essential Links </q-item-label>
<EssentialLink v-for="link in linksList" :key="link.title" v-bind="link" />
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import EssentialLink, { type EssentialLinkProps } from 'components/EssentialLink.vue';
const linksList: EssentialLinkProps[] = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev',
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework',
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev',
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev',
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev',
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev',
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev',
},
];
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

View File

@@ -1,43 +0,0 @@
<template>
<q-page class="row items-center justify-evenly">
<example-component
title="Example component"
active
:todos="todos"
:meta="meta"
></example-component>
</q-page>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { Todo, Meta } from 'components/models';
import ExampleComponent from 'components/ExampleComponent.vue';
const todos = ref<Todo[]>([
{
id: 1,
content: 'ct1',
},
{
id: 2,
content: 'ct2',
},
{
id: 3,
content: 'ct3',
},
{
id: 4,
content: 'ct4',
},
{
id: 5,
content: 'ct5',
},
]);
const meta = ref<Meta>({
totalCount: 1200,
});
</script>

View File

@@ -1,3 +0,0 @@
{
"extends": "./.quasar/tsconfig.json"
}

View File

@@ -0,0 +1,30 @@
# trustcontact web components
Progetto Vue 3 (Vite) per creare Web Components custom element.
## Quick start
```bash
cd quasar/web_components
npm i
npm run dev
```
Apri il playground su `http://localhost:5173`.
## Build libreria
```bash
npm run build
```
Output in `dist/`:
- `trustcontact-web-components.es.js`
- `trustcontact-web-components.iife.js`
## Uso nel browser
```html
<script type="module" src="/path/trustcontact-web-components.es.js"></script>
<trustcontact-greeting name="Fabio"></trustcontact-greeting>
```

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Playground</title>
</head>
<body>
<h1>Web Components Playground</h1>
<trustcontact-greeting name="Fabio"></trustcontact-greeting>
<script type="module" src="/src/playground.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
{
"name": "trustcontact-web-components",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "^5.7.2",
"vite": "^6.0.7"
}
}

829
quasar/web_components/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,829 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
vue:
specifier: ^3.5.13
version: 3.5.29(typescript@5.9.3)
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.4(vite@6.4.1)(vue@3.5.29(typescript@5.9.3))
typescript:
specifier: ^5.7.2
version: 5.9.3
vite:
specifier: ^6.0.7
version: 6.4.1
packages:
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.0':
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@vitejs/plugin-vue@5.2.4':
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue/compiler-core@3.5.29':
resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==}
'@vue/compiler-dom@3.5.29':
resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==}
'@vue/compiler-sfc@3.5.29':
resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==}
'@vue/compiler-ssr@3.5.29':
resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==}
'@vue/reactivity@3.5.29':
resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==}
'@vue/runtime-core@3.5.29':
resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==}
'@vue/runtime-dom@3.5.29':
resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==}
'@vue/server-renderer@3.5.29':
resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==}
peerDependencies:
vue: 3.5.29
'@vue/shared@3.5.29':
resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vue@3.5.29:
resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/parser@7.29.0':
dependencies:
'@babel/types': 7.29.0
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@types/estree@1.0.8': {}
'@vitejs/plugin-vue@5.2.4(vite@6.4.1)(vue@3.5.29(typescript@5.9.3))':
dependencies:
vite: 6.4.1
vue: 3.5.29(typescript@5.9.3)
'@vue/compiler-core@3.5.29':
dependencies:
'@babel/parser': 7.29.0
'@vue/shared': 3.5.29
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.29':
dependencies:
'@vue/compiler-core': 3.5.29
'@vue/shared': 3.5.29
'@vue/compiler-sfc@3.5.29':
dependencies:
'@babel/parser': 7.29.0
'@vue/compiler-core': 3.5.29
'@vue/compiler-dom': 3.5.29
'@vue/compiler-ssr': 3.5.29
'@vue/shared': 3.5.29
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.6
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.29':
dependencies:
'@vue/compiler-dom': 3.5.29
'@vue/shared': 3.5.29
'@vue/reactivity@3.5.29':
dependencies:
'@vue/shared': 3.5.29
'@vue/runtime-core@3.5.29':
dependencies:
'@vue/reactivity': 3.5.29
'@vue/shared': 3.5.29
'@vue/runtime-dom@3.5.29':
dependencies:
'@vue/reactivity': 3.5.29
'@vue/runtime-core': 3.5.29
'@vue/shared': 3.5.29
csstype: 3.2.3
'@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))':
dependencies:
'@vue/compiler-ssr': 3.5.29
'@vue/shared': 3.5.29
vue: 3.5.29(typescript@5.9.3)
'@vue/shared@3.5.29': {}
csstype@3.2.3: {}
entities@7.0.1: {}
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
'@esbuild/android-arm': 0.25.12
'@esbuild/android-arm64': 0.25.12
'@esbuild/android-x64': 0.25.12
'@esbuild/darwin-arm64': 0.25.12
'@esbuild/darwin-x64': 0.25.12
'@esbuild/freebsd-arm64': 0.25.12
'@esbuild/freebsd-x64': 0.25.12
'@esbuild/linux-arm': 0.25.12
'@esbuild/linux-arm64': 0.25.12
'@esbuild/linux-ia32': 0.25.12
'@esbuild/linux-loong64': 0.25.12
'@esbuild/linux-mips64el': 0.25.12
'@esbuild/linux-ppc64': 0.25.12
'@esbuild/linux-riscv64': 0.25.12
'@esbuild/linux-s390x': 0.25.12
'@esbuild/linux-x64': 0.25.12
'@esbuild/netbsd-arm64': 0.25.12
'@esbuild/netbsd-x64': 0.25.12
'@esbuild/openbsd-arm64': 0.25.12
'@esbuild/openbsd-x64': 0.25.12
'@esbuild/openharmony-arm64': 0.25.12
'@esbuild/sunos-x64': 0.25.12
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
estree-walker@2.0.2: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fsevents@2.3.3:
optional: true
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
nanoid@3.3.11: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
source-map-js@1.2.1: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
typescript@5.9.3: {}
vite@6.4.1:
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.59.0
tinyglobby: 0.2.15
optionalDependencies:
fsevents: 2.3.3
vue@3.5.29(typescript@5.9.3):
dependencies:
'@vue/compiler-dom': 3.5.29
'@vue/compiler-sfc': 3.5.29
'@vue/runtime-dom': 3.5.29
'@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3))
'@vue/shared': 3.5.29
optionalDependencies:
typescript: 5.9.3

View File

@@ -0,0 +1,39 @@
<template>
<section class="card">
<p class="title">Hello {{ safeName }}</p>
<p class="subtitle">This is a Vue 3 custom element.</p>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{ name?: string }>();
const safeName = computed(() => (props.name && props.name.trim()) || 'there');
</script>
<style scoped>
.card {
display: inline-flex;
flex-direction: column;
gap: 0.25rem;
border: 1px solid #d1d5db;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
font-family: Inter, system-ui, -apple-system, sans-serif;
background: #ffffff;
color: #111827;
}
.title {
margin: 0;
font-weight: 700;
}
.subtitle {
margin: 0;
color: #4b5563;
font-size: 0.875rem;
}
</style>

View File

@@ -0,0 +1,3 @@
import { registerWebComponents } from './register';
registerWebComponents();

View File

@@ -0,0 +1,12 @@
import { defineCustomElement } from 'vue';
import GreetingElement from './components/Greeting.ce.vue';
const TAG_NAME = 'trustcontact-greeting';
export function registerWebComponents(): void {
if (!customElements.get(TAG_NAME)) {
customElements.define(TAG_NAME, defineCustomElement(GreetingElement));
}
}
registerWebComponents();

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
customElement: true,
}),
],
build: {
lib: {
entry: 'src/register.ts',
name: 'TrustcontactWebComponents',
formats: ['es', 'iife'],
fileName: (format) => `trustcontact-web-components.${format}.js`,
},
},
});

View File

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

2
web/static/css/app.css Normal file

File diff suppressed because one or more lines are too long

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 32 32" role="img" aria-label="Swiss flag">
<rect width="32" height="32" fill="#d52b1e"/>
<rect x="13" y="6" width="6" height="20" fill="#ffffff"/>
<rect x="6" y="13" width="20" height="6" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 271 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 48 32" role="img" aria-label="German flag">
<rect width="48" height="10.67" x="0" y="0" fill="#000000"/>
<rect width="48" height="10.67" x="0" y="10.67" fill="#dd0000"/>
<rect width="48" height="10.66" x="0" y="21.34" fill="#ffce00"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

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

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="English flag">
<rect width="48" height="32" fill="#012169"/>
<path d="M0 0 48 32M48 0 0 32" stroke="#ffffff" stroke-width="6"/>
<path d="M0 0 48 32M48 0 0 32" stroke="#c8102e" stroke-width="3"/>
<rect x="20" y="0" width="8" height="32" fill="#ffffff"/>
<rect x="0" y="12" width="48" height="8" fill="#ffffff"/>
<rect x="22" y="0" width="4" height="32" fill="#c8102e"/>
<rect x="0" y="14" width="48" height="4" fill="#c8102e"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

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

@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="United States flag">
<rect width="48" height="32" fill="#b22234"/>
<g fill="#ffffff">
<rect y="2.46" width="48" height="2.46"/>
<rect y="7.38" width="48" height="2.46"/>
<rect y="12.30" width="48" height="2.46"/>
<rect y="17.22" width="48" height="2.46"/>
<rect y="22.14" width="48" height="2.46"/>
<rect y="27.06" width="48" height="2.46"/>
</g>
<rect width="20" height="17.23" fill="#3c3b6e"/>
<g fill="#ffffff">
<circle cx="2.2" cy="2.2" r="0.7"/>
<circle cx="5.4" cy="2.2" r="0.7"/>
<circle cx="8.6" cy="2.2" r="0.7"/>
<circle cx="11.8" cy="2.2" r="0.7"/>
<circle cx="15.0" cy="2.2" r="0.7"/>
<circle cx="18.2" cy="2.2" r="0.7"/>
<circle cx="3.8" cy="4.4" r="0.7"/>
<circle cx="7.0" cy="4.4" r="0.7"/>
<circle cx="10.2" cy="4.4" r="0.7"/>
<circle cx="13.4" cy="4.4" r="0.7"/>
<circle cx="16.6" cy="4.4" r="0.7"/>
<circle cx="2.2" cy="6.6" r="0.7"/>
<circle cx="5.4" cy="6.6" r="0.7"/>
<circle cx="8.6" cy="6.6" r="0.7"/>
<circle cx="11.8" cy="6.6" r="0.7"/>
<circle cx="15.0" cy="6.6" r="0.7"/>
<circle cx="18.2" cy="6.6" r="0.7"/>
<circle cx="3.8" cy="8.8" r="0.7"/>
<circle cx="7.0" cy="8.8" r="0.7"/>
<circle cx="10.2" cy="8.8" r="0.7"/>
<circle cx="13.4" cy="8.8" r="0.7"/>
<circle cx="16.6" cy="8.8" r="0.7"/>
<circle cx="2.2" cy="11" r="0.7"/>
<circle cx="5.4" cy="11" r="0.7"/>
<circle cx="8.6" cy="11" r="0.7"/>
<circle cx="11.8" cy="11" r="0.7"/>
<circle cx="15.0" cy="11" r="0.7"/>
<circle cx="18.2" cy="11" r="0.7"/>
<circle cx="3.8" cy="13.2" r="0.7"/>
<circle cx="7.0" cy="13.2" r="0.7"/>
<circle cx="10.2" cy="13.2" r="0.7"/>
<circle cx="13.4" cy="13.2" r="0.7"/>
<circle cx="16.6" cy="13.2" r="0.7"/>
<circle cx="2.2" cy="15.4" r="0.7"/>
<circle cx="5.4" cy="15.4" r="0.7"/>
<circle cx="8.6" cy="15.4" r="0.7"/>
<circle cx="11.8" cy="15.4" r="0.7"/>
<circle cx="15.0" cy="15.4" r="0.7"/>
<circle cx="18.2" cy="15.4" r="0.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

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 48 32" role="img" aria-label="French flag">
<rect width="16" height="32" x="0" y="0" fill="#0055a4"/>
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
<rect width="16" height="32" x="32" y="0" fill="#ef4135"/>
</svg>

After

Width:  |  Height:  |  Size: 286 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 48 32" role="img" aria-label="Italian flag">
<rect width="16" height="32" x="0" y="0" fill="#009246"/>
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
<rect width="16" height="32" x="32" y="0" fill="#ce2b37"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

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

File diff suppressed because one or more lines are too long

62
web/static/vendor/theme.js vendored Normal file
View File

@@ -0,0 +1,62 @@
(function () {
var STORAGE_KEY = 'theme';
var isAuthenticated = !!window.__TC_IS_AUTHENTICATED;
var serverTheme = (window.__TC_SERVER_THEME || '').toLowerCase();
var hasStoredTheme = false;
function getPreferredTheme() {
var stored = localStorage.getItem(STORAGE_KEY);
hasStoredTheme = stored === 'dark' || stored === 'light';
if (hasStoredTheme) return stored;
if (serverTheme === 'dark' || serverTheme === 'light') return serverTheme;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function applyTheme(theme) {
var isDark = theme === 'dark';
document.documentElement.classList.toggle('dark', isDark);
var button = document.getElementById('themeToggle');
if (button) {
button.setAttribute('aria-pressed', isDark ? 'true' : 'false');
button.textContent = isDark ? 'Light mode' : 'Dark mode';
}
}
function persistTheme(theme) {
localStorage.setItem(STORAGE_KEY, theme);
hasStoredTheme = true;
}
function sendThemeToServer(theme) {
if (!isAuthenticated) return;
fetch('/preferences/theme', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'theme=' + encodeURIComponent(theme),
}).catch(function () {});
}
window.toggleTheme = function toggleTheme() {
var currentIsDark = document.documentElement.classList.contains('dark');
var nextTheme = currentIsDark ? 'light' : 'dark';
applyTheme(nextTheme);
persistTheme(nextTheme);
sendThemeToServer(nextTheme);
};
window.initThemeToggle = function initThemeToggle() {
applyTheme(document.documentElement.classList.contains('dark') ? 'dark' : 'light');
};
var initialTheme = getPreferredTheme();
applyTheme(initialTheme);
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', function (event) {
if (hasStoredTheme) return;
applyTheme(event.matches ? 'dark' : 'light');
});
}
})();

View File

@@ -11,7 +11,7 @@
<script src="/static/vendor/theme.js?v={{.BuildHash}}"></script> <script src="/static/vendor/theme.js?v={{.BuildHash}}"></script>
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}"> <link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script> <script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/flowbite.js"></script>
</head> </head>
<body class="flex min-h-screen flex-col bg-white text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100"> <body class="flex min-h-screen flex-col bg-white text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
{{template "navbar" .}} {{template "navbar" .}}
@@ -45,7 +45,7 @@
'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', '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.', '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', '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', 'private.title': 'Dashboard', 'private.back_prefix': 'Bentornato', 'private.generic': 'Benvenuto.', 'private.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', '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.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.', '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.',
@@ -60,7 +60,7 @@
'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', '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.', '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', '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', 'private.title': 'Dashboard', 'private.back_prefix': 'Signed in as', 'private.generic': 'Signed in.', 'private.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', '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.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.', 'users.new_user_modal_title': 'New user', 'users.new_user_modal_placeholder': 'Flowbite placeholder UI. Connect creation to backend route when available.',
@@ -75,7 +75,7 @@
'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', '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.', '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', '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', 'private.title': 'Dashboard', 'private.back_prefix': 'Willkommen zurück', 'private.generic': 'Willkommen.', 'private.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', '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.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.', 'users.new_user_modal_title': 'Neuer Benutzer', 'users.new_user_modal_placeholder': 'Flowbite-Placeholder-UI. Bei Bedarf mit Backend-Route verbinden.',
@@ -89,7 +89,7 @@
'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', '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.', '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', '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', 'private.title': 'Tableau de bord', 'private.back_prefix': 'Bon retour', 'private.generic': 'Bienvenue.', 'private.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', '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.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.', 'users.new_user_modal_title': 'Nouvel utilisateur', 'users.new_user_modal_placeholder': 'UI Flowbite placeholder. Connecter à une route backend si nécessaire.',
@@ -207,22 +207,60 @@
}); });
} }
function reinitFlowbiteComponents(target) { function initNavbarComponents(root) {
if (typeof window.initDropdowns === 'function') window.initDropdowns(); (root || document).querySelectorAll('[data-collapse-toggle]').forEach(function (button) {
if (typeof window.initModals === 'function') { if (button.dataset.tcBound === '1') return;
if (!target || target.id === 'usersTableContainer') { button.dataset.tcBound = '1';
window.initModals(); var targetId = button.getAttribute('data-collapse-toggle');
} var target = document.getElementById(targetId);
if (!target) return;
button.addEventListener('click', function () {
var isHidden = target.classList.contains('hidden');
target.classList.toggle('hidden', !isHidden);
button.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
});
});
var userButton = document.getElementById('user-menu-button');
var userDropdown = document.getElementById('user-dropdown');
if (userButton && userDropdown && userButton.dataset.tcBound !== '1') {
userButton.dataset.tcBound = '1';
userButton.addEventListener('click', function (event) {
event.preventDefault();
var isHidden = userDropdown.classList.contains('hidden');
userDropdown.classList.toggle('hidden', !isHidden);
userButton.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
});
}
if (!window.__tcNavbarDocBound) {
window.__tcNavbarDocBound = true;
document.addEventListener('click', function (event) {
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown');
if (!btn || !menu || menu.classList.contains('hidden')) return;
if (btn.contains(event.target) || menu.contains(event.target)) return;
menu.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown');
if (!btn || !menu) return;
menu.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
});
} }
} }
reinitFlowbiteComponents(); initNavbarComponents(document);
applyTranslations(document); applyTranslations(document);
if (typeof window.initThemeToggle === 'function') { if (typeof window.initThemeToggle === 'function') {
window.initThemeToggle(); window.initThemeToggle();
} }
document.body.addEventListener('htmx:afterSwap', function (evt) { document.body.addEventListener('htmx:afterSwap', function (evt) {
reinitFlowbiteComponents(evt.target || null); initNavbarComponents(evt.target || document);
applyTranslations(evt.target || document); applyTranslations(evt.target || document);
if (typeof window.initThemeToggle === 'function') { if (typeof window.initThemeToggle === 'function') {
window.initThemeToggle(); window.initThemeToggle();

View File

@@ -16,7 +16,6 @@
<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 dark:border-gray-700 dark:bg-gray-800 md:dark:bg-transparent"> <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 dark:border-gray-700 dark:bg-gray-800 md:dark:bg-transparent">
{{if eq .NavSection "home"}} {{if eq .NavSection "home"}}
{{if .CurrentUser}} {{if .CurrentUser}}
<li><a href="/welcome" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Welcome</a></li>
{{else}} {{else}}
<li><a href="/login" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.login">Login</a></li> <li><a href="/login" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.login">Login</a></li>
{{end}} {{end}}
@@ -53,6 +52,10 @@
<span class="block truncate text-sm text-gray-500 dark:text-gray-400">{{.CurrentUser.Email}}</span> <span class="block truncate text-sm text-gray-500 dark:text-gray-400">{{.CurrentUser.Email}}</span>
</div> </div>
<div class="py-2"> <div class="py-2">
<a href="/private" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Private</a>
{{if eq .CurrentUser.Role "admin"}}
<a href="/admin" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.admin">Admin</a>
{{end}}
<form action="/logout" method="post" class="px-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 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Logout</button> <button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Logout</button>
</form> </form>