feat: enhance routing and theme management, add error handling and improve cookie consent styles

This commit is contained in:
2026-05-06 12:59:15 +02:00
parent 8b86350a2d
commit 11c299310c
9 changed files with 164 additions and 55 deletions
+45 -18
View File
@@ -11,56 +11,83 @@
import ErrorGeneric from "./pages/errore.svelte"; import ErrorGeneric from "./pages/errore.svelte";
import NotFound from "./pages/404.svelte"; import NotFound from "./pages/404.svelte";
import { initializeStoredConsent } from "./lib/cookieConsent"; import { initializeStoredConsent } from "./lib/cookieConsent";
import { applyTheme, getSavedTheme } from "./lib/theme";
import { getErrorPath, resolvePathname } from "./lib/errorRouting";
let pathname = '/' let pathname = typeof window !== 'undefined' ? window.location.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 getRoute = (path) => {
const normalized = normalizePath(path) const normalized = resolvePathname(path)
if (normalized === '/') return 'home' if (normalized === '/') return 'home'
if (normalized === '/contatti' || normalized === '/contacts') return 'contacts' if (normalized === '/contatti' || normalized === '/contacts') return 'contacts'
if (normalized === '/403') return '403' if (normalized === '/403') return '403'
if (normalized === '/404') return '404'
if (normalized === '/500') return '500' if (normalized === '/500') return '500'
if (normalized === '/errore') return 'error' if (normalized === '/errore') return 'error'
return '404' return 'error'
} }
let currentRoute = $derived(getRoute(pathname)) let currentRoute = $derived(getRoute(pathname))
let isCenteredMain = $derived(['403', '404', '500', 'error'].includes(currentRoute)) let isCenteredMain = $derived(['403', '404', '500', 'error'].includes(currentRoute))
const navigateTo = (targetPath, replace = false) => {
const resolved = resolvePathname(targetPath)
const current = resolvePathname(window.location.pathname)
if (resolved === current) {
pathname = resolved
return
}
if (replace) {
window.history.replaceState({}, '', resolved)
} else {
window.history.pushState({}, '', resolved)
}
pathname = resolved
}
const syncRouteFromLocation = () => {
const resolved = resolvePathname(window.location.pathname)
if (resolved !== window.location.pathname) {
window.history.replaceState({}, '', resolved)
}
pathname = resolved
}
onMount(() => { onMount(() => {
initializeStoredConsent() initializeStoredConsent()
try { try {
const saved = localStorage.getItem('theme') const saved = getSavedTheme()
if (saved) { if (saved) {
document.documentElement.setAttribute('data-theme', saved) applyTheme(saved)
} else { } else {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const theme = prefersDark ? 'dark' : 'light' const theme = prefersDark ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme) applyTheme(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 syncRouteFromLocation()
const handlePopstate = () => { const handlePopstate = () => {
pathname = window.location.pathname syncRouteFromLocation()
}
const handleErrorRedirect = (event) => {
const code = event?.detail?.code ?? event?.detail?.status
navigateTo(getErrorPath(code))
} }
window.addEventListener('popstate', handlePopstate) window.addEventListener('popstate', handlePopstate)
return () => window.removeEventListener('popstate', handlePopstate) window.addEventListener('app:error-redirect', handleErrorRedirect)
return () => {
window.removeEventListener('popstate', handlePopstate)
window.removeEventListener('app:error-redirect', handleErrorRedirect)
}
}) })
</script> </script>
+17 -7
View File
@@ -26,14 +26,8 @@
border: 2px solid rgba(127, 127, 127, 0.3); border: 2px solid rgba(127, 127, 127, 0.3);
} }
.cookie-popup-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.cookie-popup-content p { .cookie-popup-content p {
margin: 10px 0 15px; margin: 15px 0 15px;
} }
.cookie-popup-title { .cookie-popup-title {
@@ -100,6 +94,22 @@
min-width: fit-content; min-width: fit-content;
} }
.cookie-toggle-item {
cursor: pointer;
display: flex;
align-items: center;
padding: 8px 0;
margin: 0;
}
.cookie-toggle-item .toggle {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
width: 100%;
}
.cookie-selector p { .cookie-selector p {
margin: 0 0 5px; margin: 0 0 5px;
} }
+8 -12
View File
@@ -1,37 +1,33 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import "./ThemeToggle.css"; import "./ThemeToggle.css";
import { applyTheme as persistTheme, getSavedTheme } from "../../lib/theme";
let theme = "light"; let theme = "light";
function applyTheme(t) { function setTheme(t) {
theme = t; theme = t;
try { persistTheme(t);
document.documentElement.setAttribute("data-theme", t);
localStorage.setItem("theme", t);
} catch (e) {
// ignore in SSR or restricted environments
}
} }
function toggle() { function toggle() {
applyTheme(theme === "dark" ? "light" : "dark"); setTheme(theme === "dark" ? "light" : "dark");
} }
onMount(() => { onMount(() => {
try { try {
const saved = localStorage.getItem("theme"); const saved = getSavedTheme();
if (saved) { if (saved) {
applyTheme(saved); setTheme(saved);
return; return;
} }
const prefersDark = const prefersDark =
window.matchMedia && window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches; window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(prefersDark ? "dark" : "light"); setTheme(prefersDark ? "dark" : "light");
} catch (e) { } catch (e) {
// safe fallback // safe fallback
applyTheme("light"); setTheme("light");
} }
}); });
</script> </script>
+2 -13
View File
@@ -14,7 +14,7 @@
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: flex-start; align-items: center;
gap: 12px; gap: 12px;
} }
@@ -68,6 +68,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
justify-content: center;
} }
.toggle-label { .toggle-label {
@@ -84,15 +85,3 @@
color: var(--muted-color, rgba(0, 0, 0, 0.6)); color: var(--muted-color, rgba(0, 0, 0, 0.6));
line-height: 1.4; line-height: 1.4;
} }
.cookie-toggle-item {
cursor: pointer;
display: flex;
padding: 8px 0;
margin: 0;
}
.cookie-toggle-item .toggle {
flex: 1;
width: 100%;
}
+38
View File
@@ -0,0 +1,38 @@
const KNOWN_PATHS = new Set([
'/',
'/contatti',
'/contacts',
'/403',
'/404',
'/500',
'/errore'
])
const PATH_ALIASES = {
'/assistenza': '/contatti',
'/supporto': '/contatti',
'/error': '/errore'
}
export const normalizePathname = (path) => {
if (!path) return '/'
const trimmed = String(path).trim()
if (!trimmed) return '/'
return trimmed.endsWith('/') && trimmed.length > 1 ? trimmed.slice(0, -1) : trimmed
}
export const resolvePathname = (path) => {
const normalized = normalizePathname(path)
const alias = PATH_ALIASES[normalized]
if (alias) return alias
if (KNOWN_PATHS.has(normalized)) return normalized
return '/404'
}
export const getErrorPath = (statusOrCode) => {
const code = String(statusOrCode ?? '').trim()
if (code === '403') return '/403'
if (code === '404') return '/404'
if (code === '500') return '/500'
return '/errore'
}
+40
View File
@@ -0,0 +1,40 @@
const THEME_KEY = 'theme'
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
export const getSavedTheme = () => {
if (typeof window === 'undefined') return null
try {
const stored = localStorage.getItem(THEME_KEY)
if (stored === 'dark' || stored === 'light') return stored
const cookieTheme = document.cookie
.split('; ')
.find((entry) => entry.startsWith(`${THEME_KEY}=`))
?.split('=')[1]
if (cookieTheme === 'dark' || cookieTheme === 'light') return cookieTheme
} catch {
// ignore storage access issues
}
return null
}
export const saveTheme = (theme) => {
if (typeof window === 'undefined') return
try {
localStorage.setItem(THEME_KEY, theme)
document.cookie = `${THEME_KEY}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`
} catch {
// ignore storage access issues
}
}
export const applyTheme = (theme) => {
if (typeof document === 'undefined') return
document.documentElement.setAttribute('data-theme', theme)
saveTheme(theme)
}
+1 -1
View File
@@ -16,7 +16,7 @@
</div> </div>
<div class="button-container"> <div class="button-container">
<Button text="HOME" href="/" /> <Button text="HOME" href="/" />
<Button text="ASSISTENZA" href="/assistenza" borderWidth="3px" color="transparent" /> <Button text="ASSISTENZA" href="/contatti" borderWidth="3px" color="transparent" />
</div> </div>
</div> </div>
<div class="error-background"> <div class="error-background">
+2 -2
View File
@@ -5,7 +5,7 @@
--muted-color: #6b7280; --muted-color: #6b7280;
--surface: #ffffff; --surface: #ffffff;
--background: rgba(246, 249, 255, 0.5); --background: rgba(246, 249, 255, 0.5);
--background-opaque: rgba(0, 0, 0, 0.12); --background-opaque: rgba(0, 0, 0, 0.2);
} }
/* Dark theme overrides (toggle by setting attribute on <html>) */ /* Dark theme overrides (toggle by setting attribute on <html>) */
@@ -15,7 +15,7 @@
--muted-color: #9ca3af; --muted-color: #9ca3af;
--surface: #000; --surface: #000;
--background: rgba(0, 0, 0, 0.8); --background: rgba(0, 0, 0, 0.8);
--background-opaque: rgba(255, 255, 255, 0.12); --background-opaque: rgba(255, 255, 255, 0.2);
} }
* { * {
+11 -2
View File
@@ -1,9 +1,18 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({ export default defineConfig(() => ({
plugins: [svelte()], plugins: [svelte()],
server: { server: {
port: 5173, port: 5173,
middlewares: [
(req, res, next) => {
if (req.url === '/' || req.url.match(/\.\w+$/) || req.url.includes('/public/')) {
return next()
} }
}) req.url = '/'
next()
}
]
}
}))