feat[ frontend / ui-core ]: implementate pagine di errore e miglioramenti UX ai componenti

Error Handling: Introdotte nuove pagine di errore dedicate (403, 404, 500 e generica) e aggiornata la logica di routing in App.svelte per la gestione degli stati di errore.

Button Component: Estese le funzionalità del componente per supportare link diretti e personalizzazione della larghezza del bordo.

Header Component: Ottimizzata la UX implementando la scomparsa dell'header durante lo scroll.

Refactoring & Style: Aggiornati gli stili globali per coerenza visiva e rimossi i file obsoleti relativi alle vecchie pagine di errore.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-04 13:55:58 +02:00
parent cfdce3b134
commit 8394e3bbca
18 changed files with 341 additions and 146 deletions
+24
View File
@@ -7,6 +7,9 @@
"": { "": {
"name": "cimaprogetti", "name": "cimaprogetti",
"version": "1.0.0", "version": "1.0.0",
"dependencies": {
"svelte-spa-router": "^4.0.0"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.5", "svelte": "^5.55.5",
@@ -970,6 +973,15 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/regexparam": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
"integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-rc.17", "version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
@@ -1042,6 +1054,18 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/svelte-spa-router": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.2.tgz",
"integrity": "sha512-T1WYYk+ymwCr5m5U+n91k4dRAT6cw5HgmoPaI/TpKgAmuugymFoSBlfzkcKIK83QH4H8gUMn4tdQ0B9enFBM6g==",
"license": "MIT",
"dependencies": {
"regexparam": "2.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ItalyPaleAle"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+58 -8
View File
@@ -3,30 +3,80 @@
import Hero from "./components/Hero/Hero.svelte"; import Hero from "./components/Hero/Hero.svelte";
import Services from "./components/Services/Services.svelte"; import Services from "./components/Services/Services.svelte";
import Footer from "./components/Footer/Footer.svelte"; import Footer from "./components/Footer/Footer.svelte";
import "./styles/App.css";
import Header from "./components/Header/Header.svelte"; import Header from "./components/Header/Header.svelte";
import Contacts from "./pages/Contacts.svelte";
import Forbidden from "./pages/403.svelte";
import ServerError from "./pages/500.svelte";
import ErrorGeneric from "./pages/errore.svelte";
import NotFound from "./pages/404.svelte";
let pathname = '/'
const normalizePath = (path) => {
if (!path) return '/'
const trimmed = path.trim()
return trimmed.endsWith('/') && trimmed.length > 1
? trimmed.slice(0, -1)
: trimmed
}
const getRoute = (path) => {
const normalized = normalizePath(path)
if (normalized === '/') return 'home'
if (normalized === '/contatti' || normalized === '/contacts') return 'contacts'
if (normalized === '/403') return '403'
if (normalized === '/500') return '500'
if (normalized === '/errore') return 'error'
return '404'
}
$: currentRoute = getRoute(pathname)
$: isCenteredMain = ['403', '404', '500', 'error'].includes(currentRoute)
onMount(() => { onMount(() => {
try { try {
const saved = localStorage.getItem('theme') const saved = localStorage.getItem('theme')
if (saved) { if (saved) {
document.documentElement.setAttribute('data-theme', saved) document.documentElement.setAttribute('data-theme', saved)
return } else {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const theme = prefersDark ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('theme', theme)
} }
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const theme = prefersDark ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('theme', theme)
} catch (e) { } catch (e) {
document.documentElement.setAttribute('data-theme', 'light') document.documentElement.setAttribute('data-theme', 'light')
} }
pathname = window.location.pathname
const handlePopstate = () => {
pathname = window.location.pathname
}
window.addEventListener('popstate', handlePopstate)
return () => window.removeEventListener('popstate', handlePopstate)
}) })
</script> </script>
<div class="app"> <div class="app">
<Header /> <Header />
<main> <main class:center-content={isCenteredMain}>
<!-- <Hero /> --> {#if currentRoute === 'home'}
<Hero />
<Services />
{:else if currentRoute === 'contacts'}
<Contacts />
{:else if currentRoute === '403'}
<Forbidden />
{:else if currentRoute === '500'}
<ServerError />
{:else if currentRoute === 'error'}
<ErrorGeneric />
{:else}
<NotFound />
{/if}
</main> </main>
<Footer /> <Footer />
</div> </div>
+5 -3
View File
@@ -2,7 +2,6 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: none;
background-color: var(--button-color); background-color: var(--button-color);
color: var(--button-text-color); color: var(--button-text-color);
padding: var(--button-padding); padding: var(--button-padding);
@@ -10,7 +9,10 @@
font: inherit; font: inherit;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: bold;
transition: transform 0.2s ease, opacity 0.2s ease, background-color 0.2s ease; transition: transform 0.2s, opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
text-decoration: none;
border: none;
box-shadow: inset 0 0 0 var(--button-border-width) var(--primary-color);
} }
.button:hover { .button:hover {
@@ -23,6 +25,6 @@
} }
.button:focus-visible { .button:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.85); outline: 2px solid var(--primary-color);
outline-offset: 3px; outline-offset: 3px;
} }
+20 -8
View File
@@ -1,16 +1,28 @@
<script> <script>
import './Button.css' import './Button.css'
export let color = '#1343F0' export let color = 'var(--primary-color)'
export let text = 'Button' export let text = 'Button'
export let textColor = '#ffffff' export let textColor = '#fff'
export let round = '8px' export let round = '8px'
export let padding = '8px 24px' export let padding = '8px 24px'
export let href = null
export let borderWidth = '0px'
</script> </script>
<button {#if href}
class="button" <a
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding};`} class="button"
> style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
{text} {href}
</button> >
{text}
</a>
{:else}
<button
class="button"
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
>
{text}
</button>
{/if}
+1 -2
View File
@@ -1,7 +1,6 @@
p, a, h4 { .footer p, .footer a, .footer h4 {
font-size: 0.7rem; font-size: 0.7rem;
margin: 0; margin: 0;
transition: color 0.2s;
} }
.footer { .footer {
+10 -2
View File
@@ -1,7 +1,15 @@
.header { .header {
position: sticky; position: fixed;
top: 0; top: 0;
left: 0;
width: 100%;
z-index: 100; z-index: 100;
transform: translateY(0);
transition: transform 0.28s ease;
}
.header.hidden {
transform: translateY(calc(-100% - 8px));
} }
.header-container { .header-container {
@@ -10,7 +18,7 @@
padding: 0 20px; padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
height: 140px; height: var(--navbar-height);
} }
.logo-img{ .logo-img{
+31 -1
View File
@@ -1,15 +1,45 @@
<script> <script>
import { onMount } from 'svelte'
import './Header.css' import './Header.css'
import Button from '../Button/Button.svelte' import Button from '../Button/Button.svelte'
let activeNav = 'home' let activeNav = 'home'
let isHidden = false
let lastScrollY = 0
const SCROLL_DELTA = 8
const TOP_OFFSET = 24
function navigate(section) { function navigate(section) {
activeNav = section activeNav = section
} }
function handleScroll() {
const currentScrollY = window.scrollY || 0
const diff = currentScrollY - lastScrollY
if (currentScrollY <= TOP_OFFSET) {
isHidden = false
} else if (diff > SCROLL_DELTA) {
isHidden = true
} else if (diff < -SCROLL_DELTA) {
isHidden = false
}
lastScrollY = currentScrollY
}
onMount(() => {
lastScrollY = window.scrollY || 0
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
}
})
</script> </script>
<header class="header" id="home"> <header class="header" class:hidden={isHidden} id="home">
<div class="header-container"> <div class="header-container">
<div class="logo"> <div class="logo">
<a href="/" aria-label="CIMA PROGETTI home"> <a href="/" aria-label="CIMA PROGETTI home">
+26
View File
@@ -0,0 +1,26 @@
<script>
import "../styles/error-pages.css";
import Button from "../components/Button/Button.svelte";
</script>
<div class="error-page">
<div class="error-content">
<div class="title">
<h1 class="title-top">ci</h1>
<h1 class="title-bottom">dispiace</h1>
<span class="divider"></span>
</div>
<div class="error-message">
<p class="error-description">Sembra che tu non abbia il permesso per arrivare fin qui...</p>
<p>...se invece si tratta di un nostro sbaglio non esitare a contattarci!</p>
</div>
<div class="button-container">
<Button text="HOME" href="/" />
<Button text="ASSISTENZA" href="/assistenza" borderWidth="3px" color="transparent" />
</div>
</div>
<div class="error-background">
<h1 class="error-code">403</h1>
<h1 class="error-text">ERROR</h1>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
<script>
import "../styles/error-pages.css";
import Button from "../components/Button/Button.svelte";
</script>
<div class="error-page">
<div class="error-content">
<div class="title">
<h1 class="title-top">ci</h1>
<h1 class="title-bottom">dispiace</h1>
<span class="divider"></span>
</div>
<div class="error-message">
<p class="error-description">Stai cercando una pagina che non esiste...</p>
<p>...se invece si tratta di un nostro sbaglio non esitare a contattarci!</p>
</div>
<Button text="HOME" href="/" />
</div>
<div class="error-background">
<h1 class="error-code">404</h1>
<h1 class="error-text">ERROR</h1>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
<script>
import "../styles/error-pages.css";
import Button from "../components/Button/Button.svelte";
</script>
<div class="error-page">
<div class="error-content">
<div class="title">
<h1 class="title-top">ci</h1>
<h1 class="title-bottom">dispiace</h1>
<span class="divider"></span>
</div>
<div class="error-message">
<p class="error-description">Anche i sistemi migliori ogni tanto crollano...</p>
<p>...l&apos;importante è intervenire tempestivamente e noi siamo gia al lavoro!</p>
</div>
<Button text="HOME" href="/" />
</div>
<div class="error-background">
<h1 class="error-code">500</h1>
<h1 class="error-text">ERROR</h1>
</div>
</div>
-28
View File
@@ -1,28 +0,0 @@
<script>
</script>
<div class="error-page">
<h1>403 - Accesso Negato</h1>
<p>Non hai i permessi per accedere a questa risorsa.</p>
</div>
<style>
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 60vh;
text-align: center;
}
h1 {
font-size: 3rem;
margin: 0;
}
p {
font-size: 1.2rem;
color: #666;
}
</style>
-28
View File
@@ -1,28 +0,0 @@
<script>
</script>
<div class="error-page">
<h1>404 - Pagina Non Trovata</h1>
<p>La pagina che stai cercando non esiste.</p>
</div>
<style>
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 60vh;
text-align: center;
}
h1 {
font-size: 3rem;
margin: 0;
}
p {
font-size: 1.2rem;
color: #666;
}
</style>
-28
View File
@@ -1,28 +0,0 @@
<script>
</script>
<div class="error-page">
<h1>500 - Errore del Server</h1>
<p>Si è verificato un errore interno del server. Riprova più tardi.</p>
</div>
<style>
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 60vh;
text-align: center;
}
h1 {
font-size: 3rem;
margin: 0;
}
p {
font-size: 1.2rem;
color: #666;
}
</style>
-28
View File
@@ -1,28 +0,0 @@
<script>
</script>
<div class="error-page">
<h1>Errore</h1>
<p>Si è verificato un errore. Per favore, riprova più tardi.</p>
</div>
<style>
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 60vh;
text-align: center;
}
h1 {
font-size: 3rem;
margin: 0;
}
p {
font-size: 1.2rem;
color: #666;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<script>
import "../styles/error-pages.css";
import Button from "../components/Button/Button.svelte";
</script>
<div class="error-page">
<div class="error-content">
<div class="title">
<h1 class="title-top">ci</h1>
<h1 class="title-bottom">dispiace</h1>
<span class="divider"></span>
</div>
<div class="error-message">
<p class="error-description">Sembra ci sia un errore...</p>
<p>...ma possiamo aiutarti anche in questo caso!</p>
</div>
<Button text="HOME" href="/" />
</div>
<div class="error-background">
<h1 class="error-text">ERROR</h1>
</div>
</div>
-9
View File
@@ -1,9 +0,0 @@
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
+90
View File
@@ -0,0 +1,90 @@
.error-page {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.error-page h1 {
margin: 0;
font-size: 3rem;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.error-description {
font-weight: bold;
}
.error-content p {
margin: 0;
}
.button-container {
display: flex;
gap: 20px;
}
.error-content button {
width: min-content;
}
.title {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
line-height: 0.9;
}
.error-page .title-top {
margin-left: -2rem;
color: var(--primary-color);
}
.title-bottom {
position: relative;
}
.divider {
display: block;
position: relative;
width: 50%;
height: 5px;
background-color: var(--primary-color);
margin-top: 8px;
}
.error-message,
.error-solution {
text-align: center;
}
.error-background {
position: absolute;
bottom: 0;
right: 0;
z-index: -1;
}
.error-background h1 {
text-align: right;
font-family: Helvetica, 'Segoe UI', sans-serif;
}
.error-code {
font-size: 4rem;
line-height: 0.4;
}
.error-text {
font-size: 15rem !important;
opacity: 0.2;
line-height: 0.8;
}
+8 -1
View File
@@ -1,5 +1,6 @@
:root { :root {
--primary-color: #0066cc; --navbar-height: 140px;
--primary-color: #1343F0;
--text-color: #111827; --text-color: #111827;
--muted-color: #6b7280; --muted-color: #6b7280;
--surface: #ffffff; --surface: #ffffff;
@@ -17,6 +18,7 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
transition: color 0.2s;
} }
body { body {
@@ -85,4 +87,9 @@ h1 {
h2 { h2 {
font-family: 'IBM Plex Mono', monospace; font-family: 'IBM Plex Mono', monospace;
}
p, a {
font-family: Helvetica, 'Segoe UI', sans-serif;
} }