setup and render template file, create new logo in different shapes

This commit is contained in:
Giuseppe Naponiello
2026-06-17 22:28:31 +02:00
parent 611c554490
commit 34a8ebc6fb
17 changed files with 494 additions and 41 deletions

View File

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

View File

@@ -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",

View File

@@ -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",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -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,20 +8,41 @@ 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{
const link = document.getElementById(activeLink);
link?.classList.add('active');
}
}

View File

@@ -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 = {

View File

@@ -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('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 = 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.id = item.id;
a.href = item.href ?? '#';
a.className = 'flex gap-2';
a.innerHTML = `${icon}<span>${item.label}</span>`;
li.appendChild(a);
}
return li;
}

View File

@@ -1,6 +1,7 @@
import { bootstrap } from "@/config/bootstrap"
bootstrap({
activeLink: 'homeLink',
onReady:() => init()
})

View File

@@ -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"). */

View 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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
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(),
};
}

View File

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