feat(front-end): admin desing improvements
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 20s
PR Checks / security-sast (pull_request) Failing after 34s
PR Checks / test-backend (pull_request) Successful in 28s
PR Checks / test-frontend (pull_request) Successful in 1m0s

This commit is contained in:
2026-03-09 18:07:22 +01:00
parent 17df0c6b9b
commit 7615b8b601
9 changed files with 204 additions and 135 deletions

View File

@@ -3,6 +3,11 @@ app.mail.admin.enabled=false
app.mail.contact-request.admin.enabled=false app.mail.contact-request.admin.enabled=false
# Admin back-office local test credentials # Admin back-office local test credentials
admin.password=local-admin-password admin.password=ciaociao
admin.session.secret=local-session-secret-for-dev-only-000000000000000000000000 admin.session.secret=local-session-secret-for-dev-only-000000000000000000000000
admin.session.ttl-minutes=480 admin.session.ttl-minutes=480
# Local media storage served by a local static server on port 8081.
media.storage.root=/Users/joe/IdeaProjects/print-calculator/storage_media
media.public.base-url=http://localhost:8081
media.ffmpeg.path=/opt/homebrew/bin/ffmpeg

View File

@@ -13,6 +13,7 @@
.page-header h1 { .page-header h1 {
margin: 0; margin: 0;
font-size: 1.45rem;
} }
.page-header p { .page-header p {
@@ -43,7 +44,8 @@ button:disabled {
} }
.create-box h2 { .create-box h2 {
margin-top: 0; margin: 0 0 var(--space-3);
font-size: 1.05rem;
} }
.form-grid { .form-grid {

View File

@@ -1,9 +1,7 @@
.section-card { .section-card {
background: var(--color-bg-card); display: flex;
border: 1px solid var(--color-border); flex-direction: column;
border-radius: var(--radius-lg); gap: var(--space-5);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
} }
.section-header { .section-header {
@@ -11,7 +9,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: var(--space-4); gap: var(--space-4);
margin-bottom: var(--space-4);
} }
.section-header h2 { .section-header h2 {
@@ -315,12 +312,12 @@ tbody tr.selected {
.error { .error {
color: var(--color-danger-500); color: var(--color-danger-500);
margin-bottom: var(--space-3); margin: 0;
} }
.success { .success {
color: #157347; color: #157347;
margin-bottom: var(--space-3); margin: 0;
} }
.status-editor { .status-editor {
@@ -411,7 +408,7 @@ button:disabled {
@media (max-width: 760px) { @media (max-width: 760px) {
.section-card { .section-card {
padding: var(--space-4); gap: var(--space-4);
} }
.section-header { .section-header {
@@ -447,10 +444,6 @@ button:disabled {
} }
@media (max-width: 520px) { @media (max-width: 520px) {
.section-card {
padding: var(--space-3);
}
th, th,
td { td {
padding: var(--space-2); padding: var(--space-2);

View File

@@ -1,9 +1,7 @@
.admin-dashboard { .admin-dashboard {
background: var(--color-bg-card); display: flex;
border: 1px solid var(--color-border); flex-direction: column;
border-radius: var(--radius-lg); gap: var(--space-5);
padding: clamp(12px, 2vw, 20px);
box-shadow: var(--shadow-sm);
} }
.dashboard-header { .dashboard-header {
@@ -11,7 +9,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: var(--space-4); gap: var(--space-4);
margin-bottom: var(--space-4);
} }
.dashboard-header h1 { .dashboard-header h1 {
@@ -294,7 +291,7 @@ tbody tr.no-results:hover {
.error { .error {
color: var(--color-danger-500); color: var(--color-danger-500);
margin-bottom: var(--space-3); margin: 0;
} }
.modal-backdrop { .modal-backdrop {
@@ -404,7 +401,7 @@ h4 {
@media (max-width: 820px) { @media (max-width: 820px) {
.admin-dashboard { .admin-dashboard {
padding: var(--space-4); gap: var(--space-4);
} }
.list-toolbar { .list-toolbar {
@@ -449,10 +446,6 @@ h4 {
} }
@media (max-width: 520px) { @media (max-width: 520px) {
.admin-dashboard {
padding: var(--space-3);
}
th, th,
td { td {
padding: var(--space-2); padding: var(--space-2);

View File

@@ -1,9 +1,7 @@
.section-card { .section-card {
background: var(--color-bg-card); display: flex;
border: 1px solid var(--color-border); flex-direction: column;
border-radius: var(--radius-lg); gap: var(--space-5);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
} }
.section-header { .section-header {
@@ -11,7 +9,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: var(--space-4); gap: var(--space-4);
margin-bottom: var(--space-4);
} }
.section-header h2 { .section-header h2 {
@@ -26,7 +23,6 @@
.alerts { .alerts {
display: grid; display: grid;
gap: var(--space-2); gap: var(--space-2);
margin-bottom: var(--space-3);
} }
.content { .content {
@@ -374,7 +370,7 @@ button:disabled {
@media (max-width: 760px) { @media (max-width: 760px) {
.section-card { .section-card {
padding: var(--space-4); gap: var(--space-4);
} }
.form-grid { .form-grid {
@@ -404,9 +400,3 @@ button:disabled {
width: 100%; width: 100%;
} }
} }
@media (max-width: 520px) {
.section-card {
padding: var(--space-3);
}
}

View File

@@ -3,10 +3,6 @@
<div class="header-copy"> <div class="header-copy">
<p class="eyebrow">Back-office media</p> <p class="eyebrow">Back-office media</p>
<h2>Media home</h2> <h2>Media home</h2>
<p>
Gestisci gallery, founders e le card "Cosa puoi ottenere" senza
toccare codice o asset statici locali.
</p>
</div> </div>
<div class="header-side"> <div class="header-side">
<div class="header-stats"> <div class="header-stats">
@@ -35,10 +31,7 @@
<div class="group-stack" *ngIf="!loading; else loadingTpl"> <div class="group-stack" *ngIf="!loading; else loadingTpl">
<section class="group-card" *ngFor="let group of sectionGroups"> <section class="group-card" *ngFor="let group of sectionGroups">
<header class="group-header"> <header class="group-header">
<div> <h3>{{ group.title }}</h3>
<h3>{{ group.title }}</h3>
<p>{{ group.description }}</p>
</div>
</header> </header>
<div class="sections"> <div class="sections">
@@ -58,7 +51,6 @@
{{ section.items.length === 1 ? "attiva" : "attive" }} {{ section.items.length === 1 ? "attiva" : "attive" }}
</span> </span>
</div> </div>
<p>{{ section.description }}</p>
</div> </div>
<div class="media-panel-meta"> <div class="media-panel-meta">
<span class="usage-pill" <span class="usage-pill"
@@ -73,27 +65,38 @@
<div class="workspace"> <div class="workspace">
<div class="upload-panel"> <div class="upload-panel">
<div class="panel-heading"> <div class="panel-heading">
<div> <h5>
<h5> {{
{{ getFormState(section.usageKey).replacingUsageId
getFormState(section.usageKey).replacingUsageId ? "Sostituisci immagine"
? "Sostituisci immagine" : "Carica immagine"
: "Carica immagine" }}
}} </h5>
</h5>
<p>{{ section.collectionHint }}</p>
</div>
</div> </div>
<div class="form-grid"> <div class="form-grid">
<label class="form-field form-field--wide"> <div class="form-field form-field--wide">
<span>File immagine</span> <span>File immagine</span>
<input <input
[id]="'media-file-' + section.usageKey"
class="sr-only"
type="file" type="file"
accept=".jpg,.jpeg,.png,.webp" accept=".jpg,.jpeg,.png,.webp"
(change)="onFileSelected(section.usageKey, $event)" (change)="onFileSelected(section.usageKey, $event)"
/> />
</label> <label
class="file-picker"
[for]="'media-file-' + section.usageKey"
>
<span class="file-picker-button">Scegli file</span>
<span class="file-picker-name">
{{
getFormState(section.usageKey).file?.name ||
"Nessun file selezionato"
}}
</span>
</label>
</div>
<div <div
class="preview-card form-field--wide" class="preview-card form-field--wide"
@@ -136,7 +139,8 @@
type="checkbox" type="checkbox"
[(ngModel)]="getFormState(section.usageKey).isPrimary" [(ngModel)]="getFormState(section.usageKey).isPrimary"
/> />
<span>Immagine primaria</span> <span class="toggle-mark" aria-hidden="true"></span>
<span>Primaria</span>
</label> </label>
</div> </div>
@@ -179,10 +183,7 @@
<div class="list-panel"> <div class="list-panel">
<div class="panel-heading"> <div class="panel-heading">
<div> <h5>Immagini attive</h5>
<h5>Immagini attive</h5>
<p>Ordina, sostituisci o rimuovi i media attualmente collegati.</p>
</div>
</div> </div>
<div <div

View File

@@ -1,14 +1,6 @@
.section-card { .section-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: calc(var(--radius-lg) + 6px);
padding: clamp(16px, 2vw, 28px);
box-shadow: var(--shadow-sm);
display: grid; display: grid;
gap: var(--space-5); gap: var(--space-5);
background:
radial-gradient(circle at top right, rgba(239, 196, 61, 0.08), transparent 28%),
var(--color-bg-card);
} }
.section-header, .section-header,
@@ -24,7 +16,7 @@
} }
.section-header { .section-header {
align-items: center; align-items: flex-start;
} }
.eyebrow { .eyebrow {
@@ -55,18 +47,23 @@
} }
.header-side { .header-side {
display: grid; display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-3); gap: var(--space-3);
justify-items: end;
} }
.header-stats { .header-stats {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-start;
gap: var(--space-2); gap: var(--space-2);
} }
.header-side > button {
align-self: flex-start;
}
.stat-chip { .stat-chip {
min-width: 128px; min-width: 128px;
padding: 0.75rem 0.9rem; padding: 0.75rem 0.9rem;
@@ -109,13 +106,13 @@
.group-stack { .group-stack {
display: grid; display: grid;
gap: var(--space-5); gap: var(--space-4);
} }
.group-card { .group-card {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: calc(var(--radius-lg) + 2px); border-radius: calc(var(--radius-lg) + 2px);
padding: clamp(14px, 2vw, 22px); padding: clamp(12px, 1.8vw, 18px);
background: linear-gradient(180deg, #fcfbf8 0%, #ffffff 100%); background: linear-gradient(180deg, #fcfbf8 0%, #ffffff 100%);
} }
@@ -124,21 +121,21 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: var(--space-4); gap: var(--space-4);
margin-bottom: var(--space-4); margin-bottom: var(--space-3);
} }
.sections { .sections {
display: grid; display: grid;
gap: var(--space-4); gap: var(--space-3);
} }
.media-panel { .media-panel {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: #ffffff; background: #ffffff;
padding: var(--space-4); padding: var(--space-3);
display: grid; display: grid;
gap: var(--space-4); gap: var(--space-3);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
} }
@@ -179,11 +176,15 @@
.media-panel-meta, .media-panel-meta,
.panel-heading { .panel-heading {
display: grid; display: grid;
gap: var(--space-2); gap: var(--space-1);
} }
.media-panel-meta { .media-panel-meta {
justify-items: end; display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: var(--space-2);
} }
.title-row { .title-row {
@@ -195,8 +196,8 @@
.workspace { .workspace {
display: grid; display: grid;
grid-template-columns: minmax(320px, 390px) minmax(0, 1fr); grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
gap: var(--space-4); gap: var(--space-3);
align-items: start; align-items: start;
} }
@@ -205,14 +206,15 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: linear-gradient(180deg, #fcfcfb 0%, #f7f7f4 100%); background: linear-gradient(180deg, #fcfcfb 0%, #f7f7f4 100%);
padding: var(--space-4); padding: var(--space-3);
} }
.form-grid { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3); gap: var(--space-2);
margin-top: var(--space-3); margin-top: var(--space-2);
margin-bottom: var(--space-3)
} }
.form-field { .form-field {
@@ -220,17 +222,73 @@
gap: var(--space-1); gap: var(--space-1);
} }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.form-field--wide { .form-field--wide {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.form-field > span, .form-field > span,
.sort-editor span { .sort-editor span {
font-size: 0.8rem; font-size: 0.76rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-weight: 600; font-weight: 600;
} }
.file-picker {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
cursor: pointer;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease;
}
.file-picker:hover {
border-color: var(--color-brand);
background: #fffef8;
}
.file-picker-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 6.25rem;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: calc(var(--radius-md) - 2px);
background: #ffffff;
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text);
white-space: nowrap;
}
.file-picker-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.95rem;
color: var(--color-text-muted);
}
input { input {
width: 100%; width: 100%;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
@@ -238,28 +296,59 @@ input {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
background: var(--color-bg-card); background: var(--color-bg-card);
font: inherit; font: inherit;
font-size: 0.95rem;
color: var(--color-text); color: var(--color-text);
} }
.toggle { .toggle {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.45rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 999px; border-radius: 999px;
padding: 0.5rem 0.75rem; padding: var(--space-2) var(--space-3);
background: var(--color-bg-card); background: var(--color-bg-card);
align-self: end; align-self: end;
justify-self: start;
width: auto;
cursor: pointer;
} }
.toggle input { .toggle input {
width: 16px; position: absolute;
height: 16px; opacity: 0;
margin: 0; pointer-events: none;
}
.toggle-mark {
width: 1.05rem;
height: 1.05rem;
border-radius: 0.3rem;
border: 1px solid var(--color-border);
background: #ffffff;
position: relative;
flex: 0 0 auto;
}
.toggle input:checked + .toggle-mark {
background: #b14fb8;
border-color: #b14fb8;
}
.toggle input:checked + .toggle-mark::after {
content: "";
position: absolute;
left: 0.31rem;
top: 0.12rem;
width: 0.22rem;
height: 0.46rem;
border: solid #ffffff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
} }
.toggle span { .toggle span {
font-size: 0.88rem; font-size: 0.84rem;
font-weight: 600; font-weight: 600;
} }
@@ -281,16 +370,16 @@ input {
.media-list { .media-list {
display: grid; display: grid;
gap: var(--space-3); gap: var(--space-2);
} }
.media-item { .media-item {
display: grid; display: grid;
grid-template-columns: 168px minmax(0, 1fr); grid-template-columns: 168px minmax(0, 1fr);
gap: var(--space-3); gap: var(--space-2);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: var(--space-3); padding: 0.75rem;
background: var(--color-bg-card); background: var(--color-bg-card);
} }
@@ -317,7 +406,7 @@ input {
.media-copy { .media-copy {
min-width: 0; min-width: 0;
display: grid; display: grid;
gap: var(--space-2); gap: var(--space-1);
} }
.media-copy-top { .media-copy-top {
@@ -346,6 +435,17 @@ input {
gap: var(--space-1); gap: var(--space-1);
} }
.upload-actions {
justify-content: flex-start;
flex-wrap: wrap;
align-items: flex-start;
gap: var(--space-2);
}
.upload-actions button {
min-width: 0;
}
button { button {
border: 0; border: 0;
border-radius: var(--radius-md); border-radius: var(--radius-md);
@@ -353,6 +453,7 @@ button {
color: var(--color-neutral-900); color: var(--color-neutral-900);
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
font-weight: 600; font-weight: 600;
line-height: 1.2;
cursor: pointer; cursor: pointer;
transition: transition:
background-color 0.2s ease, background-color 0.2s ease,
@@ -402,6 +503,15 @@ button.ghost.danger:hover:not(:disabled) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.file-picker {
flex-direction: column;
align-items: stretch;
}
.file-picker-button {
width: 100%;
}
.section-header, .section-header,
.header-side, .header-side,
.header-stats, .header-stats,
@@ -423,6 +533,6 @@ button.ghost.danger:hover:not(:disabled) {
} }
.media-panel-meta { .media-panel-meta {
justify-items: start; justify-content: flex-start;
} }
} }

View File

@@ -22,9 +22,7 @@ interface HomeMediaSectionConfig {
usageKey: HomeSectionKey; usageKey: HomeSectionKey;
groupId: 'galleries' | 'capabilities'; groupId: 'galleries' | 'capabilities';
title: string; title: string;
description: string;
preferredVariantName: 'card' | 'hero'; preferredVariantName: 'card' | 'hero';
collectionHint: string;
} }
interface HomeMediaFormState { interface HomeMediaFormState {
@@ -58,7 +56,6 @@ interface HomeMediaSectionView extends HomeMediaSectionConfig {
interface HomeMediaSectionGroup { interface HomeMediaSectionGroup {
id: HomeMediaSectionConfig['groupId']; id: HomeMediaSectionConfig['groupId'];
title: string; title: string;
description: string;
} }
@Component({ @Component({
@@ -75,14 +72,10 @@ export class AdminHomeMediaComponent implements OnInit, OnDestroy {
{ {
id: 'galleries', id: 'galleries',
title: 'Gallery e visual principali', title: 'Gallery e visual principali',
description:
'Sezioni che possono avere piu immagini attive e che impattano slider o gallery della home.',
}, },
{ {
id: 'capabilities', id: 'capabilities',
title: 'Cosa puoi ottenere', title: 'Cosa puoi ottenere',
description:
'Le quattro card della sezione servizi della home. Qui di solito e consigliata una sola immagine per card.',
}, },
]; ];
@@ -92,59 +85,42 @@ export class AdminHomeMediaComponent implements OnInit, OnDestroy {
usageKey: 'shop-gallery', usageKey: 'shop-gallery',
groupId: 'galleries', groupId: 'galleries',
title: 'Home: gallery shop', title: 'Home: gallery shop',
description:
'Immagini della sezione shop nella home. Non modifica il catalogo shop reale.',
preferredVariantName: 'card', preferredVariantName: 'card',
collectionHint: 'Gallery orizzontale. Consigliate piu immagini in formato card.',
}, },
{ {
usageType: 'HOME_SECTION', usageType: 'HOME_SECTION',
usageKey: 'founders-gallery', usageKey: 'founders-gallery',
groupId: 'galleries', groupId: 'galleries',
title: 'Home: gallery founders', title: 'Home: gallery founders',
description:
'Immagini del carousel founders nella home. Prev/next usa lordine configurato qui.',
preferredVariantName: 'hero', preferredVariantName: 'hero',
collectionHint: 'Hero slider. Consigliate immagini ampie con soggetto centrale.',
}, },
{ {
usageType: 'HOME_SECTION', usageType: 'HOME_SECTION',
usageKey: 'capability-prototyping', usageKey: 'capability-prototyping',
groupId: 'capabilities', groupId: 'capabilities',
title: 'Home: prototipazione veloce', title: 'Home: prototipazione veloce',
description:
'Card "Prototipazione veloce" nella sezione Cosa puoi ottenere.',
preferredVariantName: 'card', preferredVariantName: 'card',
collectionHint: 'Card singola. Consigliata una sola immagine 16:10.',
}, },
{ {
usageType: 'HOME_SECTION', usageType: 'HOME_SECTION',
usageKey: 'capability-custom-parts', usageKey: 'capability-custom-parts',
groupId: 'capabilities', groupId: 'capabilities',
title: 'Home: pezzi personalizzati', title: 'Home: pezzi personalizzati',
description:
'Card "Pezzi personalizzati" nella sezione Cosa puoi ottenere.',
preferredVariantName: 'card', preferredVariantName: 'card',
collectionHint: 'Card singola. Consigliata una sola immagine 16:10.',
}, },
{ {
usageType: 'HOME_SECTION', usageType: 'HOME_SECTION',
usageKey: 'capability-small-series', usageKey: 'capability-small-series',
groupId: 'capabilities', groupId: 'capabilities',
title: 'Home: piccole serie', title: 'Home: piccole serie',
description: 'Card "Piccole serie" nella sezione Cosa puoi ottenere.',
preferredVariantName: 'card', preferredVariantName: 'card',
collectionHint: 'Card singola. Consigliata una sola immagine 16:10.',
}, },
{ {
usageType: 'HOME_SECTION', usageType: 'HOME_SECTION',
usageKey: 'capability-cad', usageKey: 'capability-cad',
groupId: 'capabilities', groupId: 'capabilities',
title: 'Home: consulenza e CAD', title: 'Home: consulenza e CAD',
description:
'Card "Consulenza e CAD" nella sezione Cosa puoi ottenere.',
preferredVariantName: 'card', preferredVariantName: 'card',
collectionHint: 'Card singola. Consigliata una sola immagine 16:10.',
}, },
]; ];

View File

@@ -1,9 +1,7 @@
.section-card { .section-card {
background: var(--color-bg-card); display: flex;
border: 1px solid var(--color-border); flex-direction: column;
border-radius: var(--radius-lg); gap: var(--space-5);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
} }
.section-header { .section-header {
@@ -11,7 +9,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: var(--space-4); gap: var(--space-4);
margin-bottom: var(--space-5);
} }
h2 { h2 {
@@ -79,10 +76,12 @@ td {
.error { .error {
color: var(--color-danger-500); color: var(--color-danger-500);
margin: 0;
} }
.success { .success {
color: var(--color-success-500); color: var(--color-success-500);
margin: 0;
} }
.actions { .actions {
@@ -169,7 +168,7 @@ td {
@media (max-width: 520px) { @media (max-width: 520px) {
.section-card { .section-card {
padding: var(--space-3); gap: var(--space-4);
} }
th, th,