wip: events page

This commit is contained in:
2026-05-05 16:57:19 +02:00
parent 135aebd59d
commit 1f5db3b5db
10 changed files with 595 additions and 1 deletions
+51
View File
@@ -18,8 +18,10 @@
"@mui/material": "^7.0.0", "@mui/material": "^7.0.0",
"@vitejs/plugin-react-swc": "^4.2.3", "@vitejs/plugin-react-swc": "^4.2.3",
"analytics": "^0.8.19", "analytics": "^0.8.19",
"leaflet": "^1.9.4",
"react": "^19.3.1", "react": "^19.3.1",
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.18.4", "react-router-dom": "^7.18.4",
"react-scroll-parallax": "^3.5.0", "react-scroll-parallax": "^3.5.0",
"use-analytics": "^1.1.0", "use-analytics": "^1.1.0",
@@ -31,6 +33,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/leaflet": "^1.9.22",
"@types/node": "^25.9.3", "@types/node": "^25.9.3",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
@@ -1761,6 +1764,17 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.2", "version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
@@ -2415,6 +2429,13 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2422,6 +2443,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/leaflet": {
"version": "1.9.22",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz",
"integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.9.3", "version": "25.9.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
@@ -4184,6 +4215,12 @@
"@keyv/serialize": "^1.1.1" "@keyv/serialize": "^1.1.1"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4652,6 +4689,20 @@
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.4", "version": "7.18.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
+3
View File
@@ -29,8 +29,10 @@
"@mui/material": "^7.0.0", "@mui/material": "^7.0.0",
"@vitejs/plugin-react-swc": "^4.2.3", "@vitejs/plugin-react-swc": "^4.2.3",
"analytics": "^0.8.19", "analytics": "^0.8.19",
"leaflet": "^1.9.4",
"react": "^19.3.1", "react": "^19.3.1",
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.18.4", "react-router-dom": "^7.18.4",
"react-scroll-parallax": "^3.5.0", "react-scroll-parallax": "^3.5.0",
"use-analytics": "^1.1.0", "use-analytics": "^1.1.0",
@@ -42,6 +44,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/leaflet": "^1.9.22",
"@types/node": "^25.9.3", "@types/node": "^25.9.3",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
+9
View File
@@ -8,6 +8,7 @@ import Loading from 'components/molecules/LoadingScreen/index.js'
const MainPage = React.lazy(() => import('components/pages/MainPage/index.js')) const MainPage = React.lazy(() => import('components/pages/MainPage/index.js'))
const PeoplePage = React.lazy(() => import('components/pages/PeoplePage/index.js')) const PeoplePage = React.lazy(() => import('components/pages/PeoplePage/index.js'))
const ActivitiesPage = React.lazy(() => import('components/pages/ActivitiesPage/index.js')) const ActivitiesPage = React.lazy(() => import('components/pages/ActivitiesPage/index.js'))
const EventsPage = React.lazy(() => import('components/pages/EventsPage/index.js'))
const ContactPage = React.lazy(() => import('components/pages/ContactPage/index.js')) const ContactPage = React.lazy(() => import('components/pages/ContactPage/index.js'))
const PrivacyPage = React.lazy(() => import('components/pages/PrivacyPage/index.js')) const PrivacyPage = React.lazy(() => import('components/pages/PrivacyPage/index.js'))
const BusinessCardPage = React.lazy(() => import('components/pages/BusinessCardPage/index.js')) const BusinessCardPage = React.lazy(() => import('components/pages/BusinessCardPage/index.js'))
@@ -42,6 +43,14 @@ const router = createBrowserRouter([
</React.Suspense> </React.Suspense>
), ),
}, },
{
path: 'events',
element: (
<React.Suspense fallback={<Loading />}>
<EventsPage />
</React.Suspense>
),
},
{ {
path: 'contacts', path: 'contacts',
element: ( element: (
+6 -1
View File
@@ -101,6 +101,11 @@ const CustomToolbar: React.FC<CustomToolbarProps> = ({ drawerWidth = 240, window
<ListItemText primary="Personaggi" /> <ListItemText primary="Personaggi" />
</DrawerButton> </DrawerButton>
</ListItem> </ListItem>
<ListItem key="Eventi" disablePadding>
<DrawerButton component={RouterLink} to="/events">
<ListItemText primary="Eventi" />
</DrawerButton>
</ListItem>
<ListItem key="Contatti" disablePadding> <ListItem key="Contatti" disablePadding>
<DrawerButton component={RouterLink} to="/contacts"> <DrawerButton component={RouterLink} to="/contacts">
<ListItemText primary="Contatti" /> <ListItemText primary="Contatti" />
@@ -163,9 +168,9 @@ const CustomToolbar: React.FC<CustomToolbarProps> = ({ drawerWidth = 240, window
Compagnia d'Arme del Santo Luca Compagnia d'Arme del Santo Luca
</AppBarLink> </AppBarLink>
<Stack component="div" direction="row" spacing={2} sx={{ display: { xs: 'none', md: 'flex', alignItems: 'center' } }}> <Stack component="div" direction="row" spacing={2} sx={{ display: { xs: 'none', md: 'flex', alignItems: 'center' } }}>
<AppBarLink to="/">Home</AppBarLink>
<AppBarLink to="/activities">Attività</AppBarLink> <AppBarLink to="/activities">Attività</AppBarLink>
<AppBarLink to="/people">Personaggi</AppBarLink> <AppBarLink to="/people">Personaggi</AppBarLink>
<AppBarLink to="/events">Eventi</AppBarLink>
<Button component={RouterLink} to="/contacts" color="primary" variant="contained" sx={{ textTransform: 'none' }}> <Button component={RouterLink} to="/contacts" color="primary" variant="contained" sx={{ textTransform: 'none' }}>
<Typography variant="h6">Contattaci</Typography> <Typography variant="h6">Contattaci</Typography>
</Button> </Button>
@@ -0,0 +1,97 @@
import React from 'react'
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, Link, Stack, Typography } from '@mui/material'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import LocationOnIcon from '@mui/icons-material/LocationOn'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import type { Event } from './types.js'
interface EventDialogProps {
event: Event | null
onClose: () => void
}
const formatDateRange = (date: Date, endDate?: Date): string => {
const fmt = (d: Date) => d.toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric' })
if (!endDate) return fmt(date)
if (date.getMonth() === endDate.getMonth() && date.getFullYear() === endDate.getFullYear()) {
return `${date.getDate()}–${endDate.getDate()} ${date.toLocaleDateString('it-IT', { month: 'long', year: 'numeric' })}`
}
return `${fmt(date)} – ${fmt(endDate)}`
}
const EventDialog: React.FC<EventDialogProps> = ({ event, onClose }) => {
if (!event) return null
const osmUrl = `https://www.openstreetmap.org/?mlat=${event.location.lat}&mlon=${event.location.lng}#map=15/${event.location.lat}/${event.location.lng}`
return (
<Dialog open={!!event} onClose={onClose} maxWidth="sm" fullWidth>
{event.image && <Box component="img" src={event.image} alt={event.title} sx={{ width: '100%', objectFit: 'cover' }} />}
<DialogTitle>
<Typography variant="h5" component="span">
{event.details?.title ?? event.title}
</Typography>
</DialogTitle>
<DialogContent>
<Stack spacing={1} mb={2}>
<Stack direction="row" alignItems="center" spacing={1}>
<LocationOnIcon fontSize="small" color="primary" />
<Typography variant="body2">
{event.location.name}, {event.location.city}
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<CalendarMonthIcon fontSize="small" color="primary" />
<Typography variant="body2">{formatDateRange(event.date, event.endDate)}</Typography>
</Stack>
</Stack>
{event.details?.description && (
<>
<Divider sx={{ mb: 2 }} />
<Typography variant="body1" color="text.secondary">
{event.details.description}
</Typography>
</>
)}
{event.details?.howToReach && (
<>
<Divider sx={{ mb: 2 }} />
<Typography variant="body1" color="text.secondary">
{event.details.howToReach}
</Typography>
</>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between' }}>
<Stack direction="row" spacing={1}>
{event.link && (
<Chip
component={Link}
href={event.link}
target="_blank"
rel="noopener noreferrer"
label="Sito ufficiale"
icon={<OpenInNewIcon />}
clickable
/>
)}
<Chip
component={Link}
href={event.location.link || osmUrl}
target="_blank"
rel="noopener noreferrer"
label="Apri sulla mappa"
icon={<LocationOnIcon />}
clickable
/>
</Stack>
<Button onClick={onClose} variant="contained" sx={{ justifySelf: 'flex-end' }}>
Chiudi
</Button>
</DialogActions>
</Dialog>
)
}
export default EventDialog
@@ -0,0 +1,117 @@
import React from 'react'
import { Box, Card, CardActionArea, CardContent, CardMedia, Chip, Stack, Typography } from '@mui/material'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import LocationOnIcon from '@mui/icons-material/LocationOn'
import { InfoOutline } from '@mui/icons-material'
import type { Event } from './types.js'
interface ListViewProps {
events: Event[]
onSelect: (event: Event) => void
}
const formatDateRange = (date: Date, endDate?: Date): string => {
const fmt = (d: Date) => d.toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric' })
if (!endDate) return fmt(date)
if (date.getMonth() === endDate.getMonth() && date.getFullYear() === endDate.getFullYear()) {
return `${date.getDate()}-${endDate.getDate()} ${date.toLocaleDateString('it-IT', { month: 'long', year: 'numeric' })}`
}
return `${fmt(date)} - ${fmt(endDate)}`
}
const isUpcoming = (date: Date) => date >= new Date()
const EventCard: React.FC<{ event: Event; onSelect: (event: Event) => void }> = ({ event, onSelect }) => {
return (
<Card
key={event.id}
elevation={2}
sx={{
display: 'flex',
flexDirection: 'column',
opacity: isUpcoming(event.date) ? 1 : 0.6,
width: theme => theme.breakpoints.values.lg * 0.3,
}}>
<CardActionArea
onClick={() => onSelect(event)}
sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end' }}>
{event.image && <CardMedia component="img" height={160} image={event.image} alt={event.title} />}
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" mb={1}>
<Typography variant="h6" component="h2" sx={{ flexGrow: 1, mr: 1 }}>
{event.title}
</Typography>
{!isUpcoming(event.date) && <Chip label="Passato" size="small" color="default" />}
</Stack>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<div>
<Stack direction="row" alignItems="center" spacing={0.5} mb={0.5}>
<CalendarMonthIcon fontSize="small" color="primary" />
<Typography variant="body2" color="text.secondary">
{formatDateRange(event.date, event.endDate)}
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={0.5}>
<LocationOnIcon fontSize="small" color="primary" />
<Typography variant="body2" color="text.secondary">
{event.location.city}
</Typography>
</Stack>
</div>
<InfoOutline sx={{ color: 'text.secondary' }} />
</Stack>
</CardContent>
</CardActionArea>
</Card>
)
}
const ListView: React.FC<ListViewProps> = ({ events, onSelect }) => {
const eventGroups = React.useMemo(() => {
const groups: { upcoming: Event[]; past: Event[] } = { upcoming: [], past: [] }
for (const event of events) {
if (isUpcoming(event.date)) {
groups.upcoming.push(event)
} else {
groups.past.push(event)
}
}
groups.upcoming.sort((a, b) => a.date.getTime() - b.date.getTime())
groups.past.sort((a, b) => b.date.getTime() - a.date.getTime())
return groups
}, [events])
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'flex-start', pb: 4 }}>
{eventGroups.upcoming.length > 0 && (
<>
<Typography variant="h5" component="h2" textAlign="center" mt={4} mb={2}>
Prossimi eventi
</Typography>
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{eventGroups.upcoming.map(event => (
<EventCard key={event.id} event={event} onSelect={onSelect} />
))}
</Box>
</>
)}
{eventGroups.past.length > 0 && (
<>
<Typography variant="h5" component="h3" textAlign="center" mt={4} mb={2}>
Eventi passati
</Typography>
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{eventGroups.past.map(event => (
<EventCard key={event.id} event={event} onSelect={onSelect} />
))}
</Box>
</>
)}
</Box>
)
}
export default ListView
+109
View File
@@ -0,0 +1,109 @@
import React from 'react'
import { Box, Button, Chip, Stack, Typography, useTheme } from '@mui/material'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import LocationOnIcon from '@mui/icons-material/LocationOn'
import InfoIcon from '@mui/icons-material/InfoOutline'
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'
import { divIcon } from 'leaflet'
import 'leaflet/dist/leaflet.css'
import type { Event } from './types.js'
/** Creates a numbered teardrop pin SVG as a Leaflet divIcon */
const makeNumberedIcon = (n: number, color: string, isPast: boolean) => {
const fill = isPast ? '#9e9e9e' : color
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">
<path d="M16 0 C7.163 0 0 7.163 0 16 C0 28 16 42 16 42 C16 42 32 28 32 16 C32 7.163 24.837 0 16 0 Z"
fill="${fill}" stroke="white" stroke-width="1.5"/>
<text x="16" y="20" font-family="sans-serif" font-size="13" font-weight="bold"
fill="white" text-anchor="middle" dominant-baseline="middle">${n}</text>
</svg>`
return divIcon({
html: svg,
className: '',
iconSize: [32, 42],
iconAnchor: [16, 42],
popupAnchor: [0, -44],
})
}
interface MapViewProps {
events: Event[]
onSelect: (event: Event) => void
}
const formatDateRange = (date: Date, endDate?: Date): string => {
const fmt = (d: Date) => d.toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric' })
if (!endDate) return fmt(date)
if (date.getMonth() === endDate.getMonth() && date.getFullYear() === endDate.getFullYear()) {
return `${date.getDate()}–${endDate.getDate()} ${date.toLocaleDateString('it-IT', { month: 'long', year: 'numeric' })}`
}
return `${fmt(date)} – ${fmt(endDate)}`
}
const isUpcoming = (date: Date) => date >= new Date()
const CENTER: [number, number] = [43.5, 12.5] // roughly central Italy
const MapView: React.FC<MapViewProps> = ({ events, onSelect }) => {
const theme = useTheme()
const primaryColor = theme.palette.primary.main
const today = new Date()
const sorted = [...events].sort((a, b) => b.date.getTime() - a.date.getTime())
return (
<Box
sx={{
px: { xs: 2, md: 4, lg: 0 },
pb: 4,
'& .leaflet-container': {
height: { xs: 420, md: 560 },
width: '100%',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
},
}}>
<MapContainer center={CENTER} zoom={6} scrollWheelZoom style={{ height: 560, width: '100%' }}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>'
url="https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
/>
{sorted.map((event, idx) => (
<Marker
key={event.id}
position={[event.location.lat, event.location.lng]}
icon={makeNumberedIcon(sorted.length - idx, primaryColor, event.date < today)}>
<Popup>
<Stack justifyContent="flex-end" gap={0.5}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" gap={1}>
<Typography variant="h6" component="h2" sx={{ flexGrow: 1 }}>
{event.title}
</Typography>
{!isUpcoming(event.date) && <Chip label="Passato" size="small" color="default" />}
</Stack>
<Stack direction="row" alignItems="center" gap={0.5}>
<LocationOnIcon fontSize="small" />
<Typography variant="caption">
{event.location.name}
<br /> {event.location.city}
</Typography>
</Stack>
<Stack direction="row" alignItems="center" gap={0.5} mb={0.5}>
<CalendarMonthIcon fontSize="small" />
<Typography variant="caption">{formatDateRange(event.date, event.endDate)}</Typography>
</Stack>
<Button startIcon={<InfoIcon />} size="small" variant="contained" onClick={() => onSelect(event)}>
Dettagli
</Button>
</Stack>
</Popup>
</Marker>
))}
</MapContainer>
</Box>
)
}
export default MapView
+127
View File
@@ -0,0 +1,127 @@
import type { Event } from './types.js'
export const events: Event[] = [
{
id: '1',
title: '4 Passi nel Medioevo',
date: new Date('2026-05-03'),
endDate: new Date('2026-05-04'),
location: {
name: 'Castello dei Ronchi',
city: 'Crevalcore (BO)',
lat: 44.7559844,
lng: 11.1382366,
link: 'https://maps.app.goo.gl/eqNB13PpJ4G7Sq2DA',
},
link: 'https://www.4passinelmedioevo.com/',
image: 'https://www.4passinelmedioevo.com/wp-content/uploads/2026/04/cropped-565125388_1208800397957599_3322632688455654651_n-scaled-1.jpg',
details: {
description:
"La Compagnia d'Arme del Santo Luca partecipa al tradizionale Palio di Ferrara con uno spettacolo di combattimento storico e una mostra della vita civile medievale.",
},
},
{
id: '2',
title: 'Armi e Bagabli',
date: new Date('2026-03-21'),
endDate: new Date('2026-03-22'),
location: {
name: 'Piacenza Expo',
city: 'Piacenza (PC)',
lat: 45.0423607,
lng: 9.75375,
link: 'https://maps.app.goo.gl/FQAtyyK5zCZyaGzx9',
},
link: 'https://armiebagagli.org/',
image:
'https://scontent-fco2-1.xx.fbcdn.net/v/t39.30808-6/687039739_1615155693950230_1523012821653618722_n.jpg?_nc_cat=110&ccb=1-7&_nc_sid=2a1932&_nc_ohc=301psXNiznUQ7kNvwFIU2A_&_nc_oc=AdqgQ3sUtKe-X02NMO-pOaTgFocqAEc4rep8ZTUFvGN7e1sij1OdLeErW01aLp6NJKE&_nc_zt=23&_nc_ht=scontent-fco2-1.xx&_nc_gid=vfSFXIAfYJiRdDTqz1le-g&_nc_ss=7b2a8&oh=00_Af5QgkkEnukOrBqB9EiDmBbgNtXI4_Qi3WZ8eNzsg1MwZA&oe=69FFD320',
details: {
description:
'La più grande manifestazione italiana per operatori e appassionati della Rievocazione Storica, presso il quartiere fieristico di Piacenza, appuntamento imperdibile per chiunque operi nel settore storico/rievocativo.',
},
},
{
id: 'trecentesca',
title: 'Trecentesca',
date: new Date('2026-05-16'),
endDate: new Date('2026-05-17'),
location: {
name: 'Abbazia di Morimondo',
city: 'Morimondo (MI)',
lat: 45.351915,
lng: 8.954823,
link: 'https://maps.app.goo.gl/oV97GdTZejNjpneD8',
},
link: 'https://www.abbaziamorimondo.it/trecentesca-2026',
image:
'https://scontent-fco2-1.xx.fbcdn.net/v/t39.30808-6/645894440_26176732645298731_6691381800434721321_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=25d718&_nc_ohc=EIa29pGGa5QQ7kNvwHIRuQt&_nc_oc=AdpEUIYfsBiSh8HpwsT5KkFdSYvlZYkUOCOUl9DYdEQ9PTbGR0uaDmVjI5HKy6cugb4&_nc_zt=23&_nc_ht=scontent-fco2-1.xx&_nc_gid=EcW53pTVT3DWReZnsh_SSw&_nc_ss=7b2a8&oh=00_Af5duE5CS05V2-KRwthjoxoib2SIeP18zu5obSxr1R6J7g&oe=69FFBB66',
},
{
id: 'Palio dello Sparviero - Voci dal Medioevo di Trambaque',
title: 'Palio dello Sparviero',
date: new Date('2026-06-20'),
location: {
name: 'Castello di San Martino della Vaneza',
city: 'Cervarese Santa Croce (PD)',
lat: 45.4145888,
lng: 11.7110493,
link: 'https://maps.app.goo.gl/j1QwPm2CYL24vMQz6',
},
link: 'https://www.paliodellosparviero.it/',
image: 'https://www.paliodellosparviero.it/wp-content/uploads/2021/06/e3c1afa4-630c-480f-acdd-0a5685c5c0ae.jpg',
details: {
title: 'Palio dello Sparviero - Voci dal Medioevo di Trambaque',
},
},
{
id: '3',
title: 'Giostra del Borgo',
date: new Date('2026-06-13'),
location: {
name: 'Ippodromo',
city: 'Ferrara',
lat: 44.82578,
lng: 11.612924,
link: 'https://maps.app.goo.gl/mqgVWmm8crAz7ofUA',
},
image:
'https://scontent-fco2-1.xx.fbcdn.net/v/t39.30808-6/495026144_998527592485792_333972602704298657_n.jpg?_nc_cat=111&ccb=1-7&_nc_sid=2a1932&_nc_ohc=V-Rj6Zro94UQ7kNvwGL_LTQ&_nc_oc=AdqbGYdKAgeN0MyVzbsnRbz31W9qqEIyJMzesfZl9r0f33UcVC-00gGtdM62Y6eLZso&_nc_zt=23&_nc_ht=scontent-fco2-1.xx&_nc_gid=XaR2VZ5CeSlQhvYnG2Ofbg&_nc_ss=7b2a8&oh=00_Af6CgCkib7vEwAVeuiPwlTTjkQrBeeLqzpE2ZkxzdWmxDA&oe=69FFB224',
link: 'https://www.quintana.it',
},
{
id: 'Mantova Medievale',
title: 'Mantova Medievale',
date: new Date('2026-11-07'),
endDate: new Date('2026-11-08'),
location: {
name: 'Lungolago Gonzaga',
city: 'Mantova',
lat: 45.160947,
lng: 10.799786,
link: 'https://maps.app.goo.gl/QF5UjjLb5HRYgLxg8',
},
link: 'http://www.mantovamedievale.it/',
image:
'https://scontent-fco2-1.xx.fbcdn.net/v/t39.30808-6/475772202_928196622760603_187755049466880515_n.jpg?_nc_cat=108&ccb=1-7&_nc_sid=2a1932&_nc_ohc=GFNIVf-3cKoQ7kNvwHWpMoA&_nc_oc=AdqCNjkyT6mYI9D8sIsmAgfQdXtrVMNOthjth3dfcGCvVgxVSJSG_SVWroEcBPbyI7k&_nc_zt=23&_nc_ht=scontent-fco2-1.xx&_nc_gid=nD82EIE8zEzhzr8oQIF78A&_nc_ss=7b2a8&oh=00_Af6Jg9HJBlMy--pZ0eQp_QZ18k1b-hSmMDp-zUC6ID5Yrw&oe=69FFE2CC',
},
{
id: '5',
title: 'Usi & Costumi',
date: new Date('2026-11-07'),
endDate: new Date('2026-11-08'),
location: {
name: 'Ferrara Fiere',
city: 'Ferrara',
lat: 44.8141398,
lng: 11.584555,
link: 'https://maps.app.goo.gl/q6MgoYrjm1uGxrrD8',
},
link: 'https://www.usiecostumi.org/',
image:
'https://scontent-fco2-1.xx.fbcdn.net/v/t39.30808-6/688923196_1294084136264066_3431102776522875652_n.jpg?_nc_cat=111&ccb=1-7&_nc_sid=2a1932&_nc_ohc=8DTyQeSJaEUQ7kNvwHpXmkS&_nc_oc=AdoKtRJlPuSnY0LgIEkrFSznA6aOekLn9cj09sScE7fPJEQz4YU7GSHIxHTZ0oOcJSw&_nc_zt=23&_nc_ht=scontent-fco2-1.xx&_nc_gid=6_24uMOlLU5IMhQptMgtsg&_nc_ss=7b2a8&oh=00_Af4WsF0pOUJT70MBLbOaRro0wJ1kct7-4dh_dto93ObmWw&oe=69FFD8CE',
details: {
description: 'La prima grande fiera internazionale, dedicata alla tradizione come strumento al servizio del turismo sostenibile.',
},
},
//
]
+52
View File
@@ -0,0 +1,52 @@
import React from 'react'
import { Box, Tab, Tabs, Typography } from '@mui/material'
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'
import MapIcon from '@mui/icons-material/Map'
import { Page } from 'components/templates/index.js'
import type { Event } from './types.js'
import { events } from './events.js'
import EventDialog from './EventDialog.js'
import ListView from './ListView.js'
const MapView = React.lazy(() => import('./MapView.js'))
type ViewTab = 'list' | 'map'
const EventsPage = () => {
const [tab, setTab] = React.useState<ViewTab>('list')
// const [selectedTab, setSelectedTab] = React.useState<ViewTab>('list')
const [selectedEvent, setSelectedEvent] = React.useState<Event | null>(null)
return (
<Page
title="Eventi"
sx={{
width: theme => theme.breakpoints.values.lg,
px: 'auto',
mx: 'auto',
}}>
<Typography>
Vieni a trovarci! Partecipiamo a rievocazioni storiche, fiere e manifestazioni in tutta Italia, portando la nostra passione per il Medioevo in
ogni angolo del paese. Consulta il calendario per non perdere le nostre prossime avventure!
</Typography>
<Box>
<Tabs value={tab} onChange={(_, v: ViewTab) => setTab(v)} centered aria-label="vista eventi">
<Tab icon={<FormatListBulletedIcon />} iconPosition="start" label="Lista" value="list" />
<Tab icon={<MapIcon />} iconPosition="start" label="Mappa" value="map" />
</Tabs>
</Box>
{tab === 'list' && <ListView events={events} onSelect={setSelectedEvent} />}
{tab === 'map' && (
<React.Suspense fallback={null}>
<MapView events={events} onSelect={setSelectedEvent} />
</React.Suspense>
)}
<EventDialog event={selectedEvent} onClose={() => setSelectedEvent(null)} />
</Page>
)
}
export default EventsPage
+24
View File
@@ -0,0 +1,24 @@
export interface EventLocation {
name: string
city: string
lat: number
lng: number
link?: string
}
export interface EventDetails {
title?: string
description?: string
howToReach?: string
}
export interface Event {
id: string
title: string
date: Date
endDate?: Date
location: EventLocation
details?: EventDetails
image?: string
link?: string
}