feat: enhance routing and theme management, add error handling and improve cookie consent styles
This commit is contained in:
+45
-18
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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%;
|
|
||||||
}
|
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user