stato intermedio

This commit is contained in:
fabio
2026-03-01 14:36:26 +01:00
parent e0ef48f6fd
commit b852f656d4
25 changed files with 4230 additions and 390 deletions

View File

@@ -31,10 +31,12 @@
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300 dark:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<button type="button" class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
<span class="inline-flex items-center justify-center">
<svg class="h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800" id="user-dropdown">

View File

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

View File

@@ -10,16 +10,34 @@
</button>
</div>
<form id="usersFilters" class="grid gap-3 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 md:grid-cols-4" hx-get="/admin/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
<div class="md:col-span-2">
<form id="usersFilters" class="flex gap-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 md:grid-cols-4" hx-get="/admin/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
<div class="flex-auto">
<label for="users-q" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white" data-i18n="users.search">Search</label>
<input id="users-q" type="text" name="q" placeholder="Cerca nome o email" data-i18n-placeholder="users.search_placeholder" value="{{.PageData.Q}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400">
</div>
<div>
<div style="max-width: 100px;" class="flex-none">
<label for="users-size" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white" data-i18n="users.page_size">Page size</label>
<input id="users-size" type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400">
<div class="relative">
<input id="users-size-value" type="hidden" name="pageSize" value="{{.PageData.PageSize}}">
<button id="users-size-button" data-dropdown-toggle="users-size-dropdown" type="button" class="inline-flex w-full items-center justify-between rounded-lg border border-gray-300 bg-gray-50 px-3 py-2.5 text-sm text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-700">
<span id="users-size-label">{{.PageData.PageSize}}</span>
<svg class="ms-2 h-2.5 w-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" />
</svg>
</button>
<div id="users-size-dropdown" class="z-50 mt-1 hidden w-full divide-y divide-gray-100 rounded-lg bg-white shadow-sm dark:divide-gray-600 dark:bg-gray-700">
<ul class="py-1 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="users-size-button">
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="5">5</button></li>
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="10">10</button></li>
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="20">20</button></li>
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="50">50</button></li>
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="100">100</button></li>
<li><button type="button" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600" data-page-size-option="500">500</button></li>
</ul>
</div>
</div>
</div>
<div class="flex items-end">
<div style="max-width: 120px;" class="flex-auto self-end">
<button type="submit" class="w-full rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300" data-i18n="users.search_button">Cerca</button>
</div>
<input type="hidden" name="sort" value="{{.PageData.Sort}}">

View File

@@ -4,7 +4,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<script src="/static/vendor/theme.js"></script>
<script>
window.__TC_IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}};
window.__TC_SERVER_THEME = {{printf "%q" .UserTheme}};
</script>
<script src="/static/vendor/theme.js?v={{.BuildHash}}"></script>
<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>
@@ -30,6 +34,8 @@
(function () {
var DEFAULT_LANG = 'it';
var STORAGE_KEY = 'tc_lang';
var SERVER_LANG = {{printf "%q" .UserLang}};
var IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}};
var dictionaries = {
it: {
'nav.open_main_menu': 'Apri menu principale', 'nav.open_user_menu': 'Apri menu utente', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
@@ -96,9 +102,33 @@
dictionaries.de_ch = Object.assign({}, dictionaries.de);
dictionaries.fr_ch = Object.assign({}, dictionaries.fr);
function normalizeLang(lang) {
if (!lang) return '';
return String(lang).trim().toLowerCase().replace('-', '_');
}
function isSupportedLang(lang) {
return !!dictionaries[normalizeLang(lang)];
}
function persistLangPreference(lang) {
if (!IS_AUTHENTICATED) return;
fetch('/preferences/lang', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ lang: lang })
}).catch(function () {});
}
function getLang() {
var serverLang = normalizeLang(SERVER_LANG);
if (IS_AUTHENTICATED && isSupportedLang(serverLang)) {
localStorage.setItem(STORAGE_KEY, serverLang);
return serverLang;
}
var stored = localStorage.getItem(STORAGE_KEY);
return dictionaries[stored] ? stored : DEFAULT_LANG;
return isSupportedLang(stored) ? normalizeLang(stored) : DEFAULT_LANG;
}
function t(key, lang) {
@@ -171,13 +201,8 @@
};
flag.src = flags[lang] || '/static/vendor/flags/it.svg';
flag.alt = labels[lang] || 'Lingua';
if (lang === 'de_ch' || lang === 'fr_ch') {
flag.style.width = '32px';
flag.style.height = '32px';
} else {
flag.style.width = '48px';
flag.style.height = '32px';
}
flag.style.width = '32px';
flag.style.height = '22px';
}
(root || document).querySelectorAll('[data-i18n]').forEach(function (el) {
@@ -196,7 +221,9 @@
document.querySelectorAll('[data-lang-select]').forEach(function (btn) {
btn.addEventListener('click', function () {
localStorage.setItem(STORAGE_KEY, btn.getAttribute('data-lang-select'));
var selectedLang = normalizeLang(btn.getAttribute('data-lang-select'));
localStorage.setItem(STORAGE_KEY, selectedLang);
persistLangPreference(selectedLang);
applyTranslations(document);
var dropdownInstance = window.FlowbiteInstances && window.FlowbiteInstances.getInstance
? window.FlowbiteInstances.getInstance('Dropdown', 'lang-dropdown')
@@ -212,6 +239,28 @@
});
});
document.querySelectorAll('[data-page-size-option]').forEach(function (btn) {
btn.addEventListener('click', function () {
var value = btn.getAttribute('data-page-size-option');
var input = document.getElementById('users-size-value');
var label = document.getElementById('users-size-label');
if (input) input.value = value;
if (label) label.textContent = value;
var dropdownInstance = window.FlowbiteInstances && window.FlowbiteInstances.getInstance
? window.FlowbiteInstances.getInstance('Dropdown', 'users-size-dropdown')
: null;
if (dropdownInstance && typeof dropdownInstance.hide === 'function') {
dropdownInstance.hide();
return;
}
var button = document.getElementById('users-size-button');
var dropdown = document.getElementById('users-size-dropdown');
if (dropdown) dropdown.classList.add('hidden');
if (button) button.setAttribute('aria-expanded', 'false');
});
});
function reinitFlowbiteComponents(target) {
if (typeof window.initDropdowns === 'function') window.initDropdowns();
if (typeof window.initModals === 'function') {

View File

@@ -1,7 +1,7 @@
{{define "language_dropdown"}}
<div class="relative flex items-center gap-2">
<img id="lang-flag" class="rounded object-cover" src="/static/vendor/flags/it.svg" alt="Italiano" style="width:48px;height:32px;">
<button id="lang-menu-button" data-dropdown-toggle="lang-dropdown" type="button" class="inline-flex h-10 items-center rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" aria-expanded="false">
<div class="relative flex items-center gap-2 text-sm font-small text-gray-7000">
<img id="lang-flag" class="rounded object-cover dark:outline" src="/static/vendor/flags/it.svg" alt="Italiano" style="width:32px;height:22px;">
<button id="lang-menu-button" data-dropdown-toggle="lang-dropdown" type="button" class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" aria-expanded="false">
<span id="lang-current" class="inline-block min-w-[95px]">Italiano</span>
</button>
<div id="lang-dropdown" class="z-50 my-2 hidden w-40 list-none divide-y divide-gray-100 rounded-lg bg-white text-sm shadow-sm dark:divide-gray-700 dark:bg-gray-800">

View File

@@ -30,10 +30,12 @@
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300 dark:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<button type="button" class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
<span class="inline-flex items-center justify-center">
<svg class="h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800" id="user-dropdown">

View File

@@ -39,10 +39,12 @@
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="flex items-center rounded-full bg-gray-800 text-sm focus:ring-4 focus:ring-gray-300 dark:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<button type="button" class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 font-semibold text-white">
{{if .CurrentUser.Name}}{{printf "%.1s" .CurrentUser.Name}}{{else}}{{printf "%.1s" .CurrentUser.Email}}{{end}}
<span class="inline-flex items-center justify-center">
<svg class="h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800" id="user-dropdown">

View File

@@ -4,7 +4,7 @@
<h1 class="mb-1 text-2xl font-bold text-gray-900 dark:text-white" data-i18n="login.title">Login</h1>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400" data-i18n="login.subtitle">Accedi al tuo account.</p>
<form action="/login" method="post" class="space-y-5">
<form hx-post="/login" class="space-y-5">
<div>
<label for="email" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white" data-i18n="form.email">Email</label>
<input id="email" type="text" name="email" value="{{.Email}}" class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" required />