setup and render template file, create new logo in different shapes
@@ -36,10 +36,10 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="border-b border-white shadow">
|
||||
<div id="header-brand" class="h-full w-auto">
|
||||
<a href="/" aria-label="Dynamic Collection — home" class="inline-block h-full">
|
||||
<img src="/icons/lunds-unversitet.svg" alt="Lunds universitet" width="636" height="130" class="h-full w-auto" />
|
||||
<header class="shadow">
|
||||
<div id="header-brand" class="h-full w-auto p-2">
|
||||
<a href="/" aria-label="Dynamic Collection — home" class="inline-block h-full w-auto">
|
||||
<img src="/logo/logo_dyncoll_graphic.png" alt="Dynamic Collection" width="432" height="320" class="h-full w-auto" />
|
||||
</a>
|
||||
</div>
|
||||
<nav id="header-menu" aria-label="Menu principale" class="h-full w-auto"></nav>
|
||||
@@ -47,13 +47,68 @@
|
||||
|
||||
<main id="main-container"></main>
|
||||
|
||||
<aside class="user-panel" aria-label="Area utente">
|
||||
<div class="user-card"></div>
|
||||
<nav aria-label="Menu utente"></nav>
|
||||
</aside>
|
||||
<!-- Il pannello utente (#user-panel) e il backdrop sono iniettati via TS
|
||||
(setUserPanel) solo per gli utenti autenticati. -->
|
||||
|
||||
<footer id="main-footer" class="h-[500px] w-full">
|
||||
<img src="/logo/logo_footer_dark.png" class="h-1/2 w-auto" alt="">
|
||||
<footer id="main-footer">
|
||||
<div id="footer-info" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div id="footer-scope">
|
||||
<p><span class="fw-bold">Dynamic Collections</span> is an initiative of</p>
|
||||
<ul class="">
|
||||
<li><a href="https://www.darklab.lu.se/" class="link link-hover" target="_blank">DARKLab</a></li>
|
||||
<li><a href="https://www.lu.se/" class="link link-hover" target="_blank">Lund University</a></li>
|
||||
<li><a href="http://vcg.isti.cnr.it/" class="link link-hover" target="_blank">Visual Computing Lab (ISTI-CNR)</a></li>
|
||||
<li><p class="mt-3">© 2022 - <span id="currentYear"></span> <span class="text-(--dc-secondary-dark)">DARKLab</span></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="footer-license">
|
||||
<h6 class="fw-bold">Licensing Information</h6>
|
||||
<ul class="">
|
||||
<li>
|
||||
<strong>Text & Documentation:</strong> <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" class="link link-hover">CC BY 4.0</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Database Content:</strong> <a href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" class="link link-hover">CC0 1.0 Universal</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Source Code:</strong> <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" class="link link-hover" target="_blank" title="Affero GPL 3.0 deed">GNU Affero General Public License</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Images & 3D Models:</strong> Individual licensing applies
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="footer-repo">
|
||||
<ul class="list-unstyled small">
|
||||
<li>
|
||||
<a href="https://github.com/LundDarkLab/adc" class="link link-hover" target="_blank" title="source code repository [external link]">
|
||||
<i class=""></i>
|
||||
Source code on GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://codeberg.org/LundDarkLab/adc" class="link link-hover" target="_blank" title="source code mirror on Codeberg [external link]">
|
||||
<i class=""></i>
|
||||
Mirror on Codeberg
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://lunddarklab.github.io/adc/" class="link link-hover" target="_blank" title="platform documentation [external link]">
|
||||
<i class=""></i>
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://doi.org/10.5281/zenodo.20669650" class="link link-hover" target="_blank" title="citable software archive on Zenodo [external link]">
|
||||
<i class=""></i>
|
||||
Zenodo — DOI 10.5281/zenodo.20669650
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="footer-menu"></div>
|
||||
</div>
|
||||
<div id="footer-logo" class="mt-10 pt-5 border-t border-neutral-300"></div>
|
||||
</footer>
|
||||
<script type="module" src="./src/pages/index/index.ts"></script>
|
||||
</body>
|
||||
|
||||
9
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "dyncoll-frontend",
|
||||
"version": "3.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/rajdhani": "^5.2.7",
|
||||
"@fontsource/titillium-web": "^5.2.8",
|
||||
"axios": "^1.17.0",
|
||||
"chart.js": "^4.5.1",
|
||||
@@ -114,6 +115,14 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/rajdhani": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/rajdhani/-/rajdhani-5.2.7.tgz",
|
||||
"integrity": "sha512-7Gy10U688fCdeFfYKebUF2TZotdgH/ghKyMsseXPmB60lpaUHC8aoCSJl5/OpZ+KHKSU2TqBfKfteVkcIXxTAQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/titillium-web": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/titillium-web/-/titillium-web-5.2.8.tgz",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run --coverage",
|
||||
"test": "vitest run --coverage --passWithNoTests",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -24,6 +24,7 @@
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/rajdhani": "^5.2.7",
|
||||
"@fontsource/titillium-web": "^5.2.8",
|
||||
"axios": "^1.17.0",
|
||||
"chart.js": "^4.5.1",
|
||||
|
||||
1
frontend/public/logo/LundUniversity_footer.svg
Normal file
|
After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
frontend/public/logo/logo_dyncoll.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
frontend/public/logo/logo_dyncoll_graphic.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 1006 KiB |
BIN
frontend/public/logo/logo_footer_white.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@@ -1,5 +1,6 @@
|
||||
import "@/styles/main.css"
|
||||
import { setHeaderMenu } from "./ui";
|
||||
import { renderUI } from "./ui";
|
||||
import type { AuthUser } from "@/shared/auth";
|
||||
|
||||
|
||||
type BootstrapOptions = {
|
||||
@@ -7,17 +8,38 @@ type BootstrapOptions = {
|
||||
onReady?: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
// Stub temporaneo: l'endpoint /api/user non è ancora disponibile, quindi
|
||||
// simuliamo un utente autenticato (ADMIN) per provare header + pannello.
|
||||
// Quando l'endpoint sarà pronto: importare getAuthUser da "@/shared/auth" e
|
||||
// sostituire MOCK_USER con `await getAuthUser()` nel punto indicato sotto.
|
||||
const MOCK_USER: AuthUser = {
|
||||
id: 1,
|
||||
name: 'Dev User',
|
||||
email: 'dev@example.test',
|
||||
role_id: 1, // ROLE.ADMIN → mostra anche la voce "Admin"
|
||||
setup_status: 'complete',
|
||||
};
|
||||
|
||||
export async function bootstrap(options: BootstrapOptions = {}): Promise<void> {
|
||||
const {
|
||||
activeLink = null,
|
||||
onReady,
|
||||
} = options;
|
||||
|
||||
setHeaderMenu(null);
|
||||
// render() è idempotente: lo richiamiamo dopo aver risolto l'utente.
|
||||
const render = (user: AuthUser | null) => {
|
||||
renderUI(user);
|
||||
if (activeLink !== null) { setActiveLink(activeLink); }
|
||||
};
|
||||
|
||||
if(activeLink !== null){setActiveLink(activeLink);}
|
||||
// 1. paint immediato come ospite (nessuna attesa di rete)
|
||||
render(null);
|
||||
|
||||
await onReady?.();
|
||||
|
||||
// 2. upgrade: qui andrà `await getAuthUser()`; per ora usiamo lo stub.
|
||||
const user: AuthUser | null = MOCK_USER;
|
||||
if (user) { render(user); }
|
||||
}
|
||||
|
||||
function setActiveLink(activeLink:string): void{
|
||||
|
||||
@@ -5,7 +5,11 @@ type IconName =
|
||||
| 'shield'
|
||||
| 'book-open'
|
||||
| 'log-in'
|
||||
| 'menu';
|
||||
| 'menu'
|
||||
| 'log-out'
|
||||
| 'user'
|
||||
| 'settings'
|
||||
| 'layout-dashboard';
|
||||
|
||||
type Visibility =
|
||||
| 'always'
|
||||
@@ -50,6 +54,33 @@ export function getVisibleItems(isLoggedIn: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Menu utente (pannello laterale) -------------------------------------
|
||||
// Mostrato solo agli utenti autenticati. Le voci con `roles` sono visibili
|
||||
// solo ai ruoli elencati; senza `roles` sono visibili a tutti i loggati.
|
||||
// Per spostare/aggiungere una voce basta intervenire qui, senza toccare il DOM.
|
||||
|
||||
type PanelAction = 'logout';
|
||||
|
||||
export type UserPanelItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
ico: IconName;
|
||||
href?: string; // se presente, la voce è un link
|
||||
action?: PanelAction; // se presente, la voce è un pulsante con comportamento
|
||||
roles?: RoleId[]; // assente = tutti gli autenticati
|
||||
};
|
||||
|
||||
const userPanel: UserPanelItem[] = [
|
||||
{ id: 'dashboardLink', label: 'Dashboard', ico: 'layout-dashboard', href: '/dashboard' },
|
||||
{ id: 'profileLink', label: 'Profile', ico: 'user', href: '/profile' },
|
||||
{ id: 'adminLink', label: 'Admin', ico: 'settings', href: '/admin', roles: [ROLE.ADMIN, ROLE.SUPERVISOR] },
|
||||
{ id: 'logoutBtn', label: 'Logout', ico: 'log-out', action: 'logout' },
|
||||
];
|
||||
|
||||
export function getUserPanelItems(roleId: number): UserPanelItem[] {
|
||||
return userPanel.filter(item => !item.roles || item.roles.includes(roleId as RoleId));
|
||||
}
|
||||
|
||||
// Tipi per sidebar
|
||||
// type SidebarAction = 'logout';
|
||||
// type SidebarItem = {
|
||||
|
||||
@@ -1,22 +1,217 @@
|
||||
import type { AuthUser } from "@/shared/auth";
|
||||
import { getVisibleItems } from "./menuTypes";
|
||||
import { createIcons, House, MapPin, Award, Shield, BookOpen, LogIn, Menu } from 'lucide';
|
||||
import { logoutUser } from "@/shared/auth";
|
||||
import { getVisibleItems, getUserPanelItems, type UserPanelItem } from "./menuTypes";
|
||||
import { getCurrentDate } from "@/shared/utils";
|
||||
import {
|
||||
createIcons,
|
||||
House, MapPin, Award, Shield, BookOpen, LogIn, Menu,
|
||||
LogOut, User, Settings, LayoutDashboard,
|
||||
} from 'lucide';
|
||||
|
||||
export function setHeaderMenu(user: AuthUser | null): void {
|
||||
type Img = {
|
||||
src: string;
|
||||
alt:string;
|
||||
class?:string | null;
|
||||
}
|
||||
|
||||
type MenuLink = {
|
||||
id: string;
|
||||
href: string;
|
||||
ico: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const footerImg: Img[] = [
|
||||
{src: '/logo/LundUniversity_footer.svg', alt: 'Lund University Logo'},
|
||||
{src: '/logo/logo_footer_white.png', alt:'DarkLab Logo'}
|
||||
];
|
||||
|
||||
// Set unico di icone Lucide usate nelle isole (header, pannello, footer).
|
||||
const icons = { House, MapPin, Award, Shield, BookOpen, LogIn, Menu, LogOut, User, Settings, LayoutDashboard };
|
||||
|
||||
export function renderUI(user: AuthUser | null): void {
|
||||
setLogoHeader();
|
||||
setHeaderMenu(user);
|
||||
setUserPanel(user);
|
||||
setFooterContent(user);
|
||||
setCopyrightCurrentYear();
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function setCopyrightCurrentYear(){
|
||||
const currentYear = document.getElementById("currentYear");
|
||||
if(!currentYear) return;
|
||||
currentYear.textContent = String(getCurrentDate().year);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brand
|
||||
// Sorgente unica del logo header: il browser sceglie nativamente per breakpoint
|
||||
// (scarica una sola immagine, reattivo al resize, nessun flash).
|
||||
const headerLogo = {
|
||||
alt: 'Dynamic Collection',
|
||||
fallback: '/logo/logo_dyncoll_graphic.png',
|
||||
sources: [
|
||||
{ media: '(min-width: 48rem)', src: '/logo/logo_dyncoll.png' },
|
||||
],
|
||||
};
|
||||
|
||||
function setLogoHeader(): void{
|
||||
const anchor = document.querySelector<HTMLAnchorElement>('#header-brand a');
|
||||
if(!anchor) return;
|
||||
|
||||
const picture = document.createElement('picture');
|
||||
picture.className = 'block h-full';
|
||||
|
||||
headerLogo.sources.forEach(s => {
|
||||
const source = document.createElement('source');
|
||||
source.media = s.media;
|
||||
source.srcset = s.src;
|
||||
picture.appendChild(source);
|
||||
});
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = headerLogo.fallback;
|
||||
img.alt = headerLogo.alt;
|
||||
img.width = 432;
|
||||
img.height = 320;
|
||||
img.className = 'h-full w-auto';
|
||||
|
||||
picture.appendChild(img);
|
||||
anchor.replaceChildren(picture);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header menu (link principali sempre visibili; ultimo slot = Login | hamburger)
|
||||
function setHeaderMenu(user: AuthUser | null): void {
|
||||
const nav = document.getElementById('header-menu');
|
||||
if (!nav) {
|
||||
console.error('Header target element not found');
|
||||
return;
|
||||
if (!nav) return;
|
||||
buildMenu(nav, getVisibleItems(user !== null));
|
||||
}
|
||||
|
||||
function setFooterContent(user: AuthUser | null): void{
|
||||
const footerMenu = document.getElementById('footer-menu');
|
||||
if(footerMenu){
|
||||
buildMenu(footerMenu, getVisibleItems(user !== null), { exclude: ['userMenuToggle'] });
|
||||
}
|
||||
const items = getVisibleItems(false);
|
||||
console.log(user);
|
||||
items.forEach(link =>{
|
||||
addFooterLogo();
|
||||
}
|
||||
|
||||
function addFooterLogo(){
|
||||
const footerLogo = document.getElementById('footer-logo');
|
||||
if(!footerLogo) return;
|
||||
footerLogo.replaceChildren(...footerImg.map(img => {
|
||||
const el = document.createElement('img');
|
||||
el.src = img.src;
|
||||
el.alt = img.alt;
|
||||
return el;
|
||||
}));
|
||||
}
|
||||
|
||||
function buildMenu(dom: HTMLElement, links: MenuLink[], opts: { exclude?: string[] } = {}): void {
|
||||
const exclude = opts.exclude ?? [];
|
||||
const isFooter = dom.id === 'footer-menu';
|
||||
dom.replaceChildren(...links
|
||||
.filter(link => !exclude.includes(link.id))
|
||||
.map(link => {
|
||||
const a = document.createElement('a');
|
||||
a.id = link.id;
|
||||
a.href = link.href
|
||||
a.classList.add('h-14', 'w-10');
|
||||
a.innerHTML = `<i data-lucide="${link.ico}"></i><span>${link.label || ''}</span>`;
|
||||
nav.appendChild(a);
|
||||
})
|
||||
createIcons({ icons: { House, MapPin, Award, Shield, BookOpen, LogIn, Menu }});
|
||||
a.href = link.href;
|
||||
a.classList.add('flex');
|
||||
if(isFooter){ a.classList.add('link', 'link-hover','gap-2', 'mb-2'); }
|
||||
const label = link.label ? `<span>${link.label}</span>` : '';
|
||||
a.innerHTML = `<i data-lucide="${link.ico}"></i>${label}`;
|
||||
return a;
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pannello utente (solo loggati): pannello laterale da destra, sotto l'header,
|
||||
// con backdrop leggero. L'hamburger (#userMenuToggle) resta sempre visibile e
|
||||
// fa da toggle. Replica il comportamento di v1 senza coprire l'header.
|
||||
const PANEL_ID = 'user-panel';
|
||||
const BACKDROP_ID = 'user-panel-backdrop';
|
||||
let escHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
function setUserPanel(user: AuthUser | null): void {
|
||||
// pulizia: rimuove eventuale pannello precedente (re-render / logout)
|
||||
document.getElementById(PANEL_ID)?.remove();
|
||||
document.getElementById(BACKDROP_ID)?.remove();
|
||||
if (escHandler) { document.removeEventListener('keydown', escHandler); escHandler = null; }
|
||||
|
||||
const toggle = document.getElementById('userMenuToggle') as HTMLAnchorElement | null;
|
||||
if (!user || !toggle) return;
|
||||
|
||||
// backdrop (parte sotto l'header, vedi CSS)
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.id = BACKDROP_ID;
|
||||
backdrop.className = 'user-panel-backdrop';
|
||||
|
||||
// pannello
|
||||
const panel = document.createElement('aside');
|
||||
panel.id = PANEL_ID;
|
||||
panel.className = 'user-panel';
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
panel.setAttribute('aria-label', 'Menu utente');
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'menu w-full p-4';
|
||||
getUserPanelItems(user.role_id).forEach(item => {
|
||||
list.appendChild(buildPanelEntry(item));
|
||||
});
|
||||
panel.appendChild(list);
|
||||
|
||||
document.body.append(backdrop, panel);
|
||||
|
||||
// --- apertura / chiusura ---
|
||||
const isOpen = () => panel.classList.contains('is-open');
|
||||
const open = () => {
|
||||
panel.classList.add('is-open');
|
||||
backdrop.classList.add('is-open');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
panel.querySelector<HTMLElement>('a, button')?.focus();
|
||||
};
|
||||
const close = () => {
|
||||
panel.classList.remove('is-open');
|
||||
backdrop.classList.remove('is-open');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
const toggleOpen = () => (isOpen() ? close() : open());
|
||||
|
||||
// accessibilità sull'hamburger
|
||||
toggle.setAttribute('role', 'button');
|
||||
toggle.setAttribute('aria-controls', PANEL_ID);
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.setAttribute('aria-haspopup', 'true');
|
||||
|
||||
toggle.addEventListener('click', e => { e.preventDefault(); toggleOpen(); });
|
||||
backdrop.addEventListener('click', close);
|
||||
escHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen()) { close(); toggle.focus(); }
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
function buildPanelEntry(item: UserPanelItem): HTMLLIElement {
|
||||
const li = document.createElement('li');
|
||||
const icon = `<i data-lucide="${item.ico}"></i>`;
|
||||
if (item.action === 'logout') {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.id = item.id;
|
||||
btn.className = 'flex gap-2 w-full text-left';
|
||||
btn.innerHTML = `${icon}<span>${item.label}</span>`;
|
||||
btn.addEventListener('click', () => { void logoutUser(); });
|
||||
li.appendChild(btn);
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
a.id = item.id;
|
||||
a.href = item.href ?? '#';
|
||||
a.className = 'flex gap-2';
|
||||
a.innerHTML = `${icon}<span>${item.label}</span>`;
|
||||
li.appendChild(a);
|
||||
}
|
||||
return li;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bootstrap } from "@/config/bootstrap"
|
||||
|
||||
bootstrap({
|
||||
activeLink: 'homeLink',
|
||||
onReady:() => init()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createIcons, TriangleAlert } from 'lucide';
|
||||
import { escapeHTML } from '../shared/utils';
|
||||
import { escapeHTML } from '../utils';
|
||||
|
||||
export interface ConfirmOptions {
|
||||
/** Titolo del modal (default: "Conferma"). */
|
||||
|
||||
36
frontend/src/shared/utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Escape dei caratteri speciali HTML, per inserire testo non fidato in innerHTML
|
||||
// senza rischi di injection.
|
||||
const HTML_ESCAPES: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
|
||||
export function escapeHTML(input: string): string {
|
||||
return input.replace(/[&<>"']/g, ch => HTML_ESCAPES[ch] ?? ch);
|
||||
}
|
||||
|
||||
// Scomposizione di una data nei suoi componenti (ora locale).
|
||||
export interface CurrentDate {
|
||||
year: number; // es. 2026
|
||||
month: number; // 1-12 (1-indicizzato, leggibile dall'uomo)
|
||||
day: number; // giorno del mese, 1-31
|
||||
hour: number; // 0-23
|
||||
minutes: number; // 0-59
|
||||
seconds: number; // 0-59
|
||||
}
|
||||
|
||||
// Ritorna anno/mese/giorno/ora/minuti/secondi della data fornita (default: adesso).
|
||||
// Il parametro opzionale rende la funzione facilmente testabile.
|
||||
export function getCurrentDate(date: Date = new Date()): CurrentDate {
|
||||
return {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate(),
|
||||
hour: date.getHours(),
|
||||
minutes: date.getMinutes(),
|
||||
seconds: date.getSeconds(),
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
@import '@fontsource/titillium-web/400.css';
|
||||
@import '@fontsource/titillium-web/700.css'; /* per grassetto vero */
|
||||
@import '@fontsource/rajdhani/500.css';
|
||||
@import '@fontsource/rajdhani/700.css'; /* display/titoli, coerente col logo */
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui"; /* NOSONAR */
|
||||
@theme {
|
||||
--font-sans: "Titillium Web", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-secondary: "Rajdhani", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root{
|
||||
--dc-header-h: 60px;
|
||||
--dc-primary: rgb(34, 69, 138);
|
||||
--dc-primary-dark: rgb(0, 15, 46);
|
||||
--dc-secondary: rgb(249, 185, 88);
|
||||
--dc-secondary-dark: rgb(241, 146, 4);
|
||||
--dc-white: rgb(255, 255, 255);
|
||||
--dc-dark-blue: rgb(0, 15, 46);
|
||||
--dc-dark-gray: rgb(34, 34, 34);
|
||||
--dc-dark-gray: rgb(52, 58, 64);
|
||||
}
|
||||
|
||||
body{
|
||||
@@ -25,8 +31,8 @@ header {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
padding: 5px 0 5px 10px;
|
||||
height: var(--dc-header-h);
|
||||
padding: 0;
|
||||
color: var(--dc-white);
|
||||
background-color: var(--dc-primary);
|
||||
display: flex;
|
||||
@@ -43,15 +49,111 @@ header {
|
||||
gap: 3px;
|
||||
}
|
||||
#header-menu a{
|
||||
width:60px;
|
||||
display: flex;
|
||||
width:70px;
|
||||
height:100%;
|
||||
margin-bottom: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#header-menu a:not(#userMenuToggle){
|
||||
border-bottom: 4px solid transparent;
|
||||
transition: all 500ms ease;
|
||||
}
|
||||
|
||||
#header-menu a.active{
|
||||
background-color: var(--dc-white);
|
||||
color: var(--dc-primary);
|
||||
border-bottom-color: var(--dc-white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#main-container{
|
||||
position:relative;
|
||||
margin-top:70px;
|
||||
margin-top: var(--dc-header-h);
|
||||
min-height: 90dvh;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
#main-footer{
|
||||
padding:50px;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
height:auto;
|
||||
width:100%;
|
||||
background-color: var(--dc-dark-gray);
|
||||
color: var(--dc-white);
|
||||
}
|
||||
|
||||
#footer-logo{
|
||||
width:100%;
|
||||
height:150px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
#header-menu a:not(#userMenuToggle):hover{
|
||||
border-bottom-color: var(--dc-white);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sotto md (48rem): voci più strette e label più piccola, così il testo resta
|
||||
sotto le icone senza mandare in overflow l'header su schermi piccoli. */
|
||||
@media (max-width: 47.999rem) {
|
||||
#header-menu a{
|
||||
width: 3rem;
|
||||
}
|
||||
#header-menu a span{
|
||||
font-size: 0.6rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Pannello utente (solo loggati): scorre da destra, parte SOTTO l'header
|
||||
(top = altezza header), con backdrop leggero. L'header resta sopra e
|
||||
cliccabile (z-index 1000 > pannello 910 > backdrop 900). */
|
||||
.user-panel{
|
||||
position: fixed;
|
||||
top: var(--dc-header-h);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(20rem, 85vw);
|
||||
z-index: 910;
|
||||
background-color: var(--dc-white);
|
||||
color: var(--dc-dark-gray);
|
||||
box-shadow: -4px 0 16px rgb(0 0 0 / 0.18);
|
||||
overflow-y: auto;
|
||||
transform: translateX(100%);
|
||||
transition: transform 300ms ease;
|
||||
}
|
||||
.user-panel.is-open{
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.user-panel-backdrop{
|
||||
position: fixed;
|
||||
top: var(--dc-header-h);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 900;
|
||||
background-color: rgb(0 0 0 / 0.25);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.user-panel-backdrop.is-open{
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.user-panel,
|
||||
.user-panel-backdrop{
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||