wip: fixed pagination

This commit is contained in:
2026-10-20 10:53:42 +02:00
parent 1f5db3b5db
commit b38bfa4e45
7 changed files with 125 additions and 22 deletions
+6 -3
View File
@@ -4,7 +4,7 @@ name: Deployment (Staging)
on: on:
# Runs on pushes targeting the default branch # Runs on pushes targeting the default branch
push: push:
branches: ['main'] branches: ["main"]
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@@ -20,8 +20,8 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: Set environment variables - name: Set environment variables
run: | run: |
@@ -44,6 +44,9 @@ jobs:
if [[ -n "${{ secrets.VITE_BC_EMAIL }}" ]]; then if [[ -n "${{ secrets.VITE_BC_EMAIL }}" ]]; then
echo "VITE_BC_EMAIL=${{ secrets.VITE_BC_EMAIL }}" >> .env echo "VITE_BC_EMAIL=${{ secrets.VITE_BC_EMAIL }}" >> .env
fi fi
if [[ -n "${{ secrets.VITE_EV_CARTO_KEY }}" ]]; then
echo "VITE_EV_CARTO_KEY=${{ secrets.VITE_EV_CARTO_KEY }}" >> .env
fi
- name: Install dependencies - name: Install dependencies
run: npm ci --omit=dev run: npm ci --omit=dev
@@ -63,8 +63,8 @@ const EventDialog: React.FC<EventDialogProps> = ({ event, onClose }) => {
</> </>
)} )}
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between' }}> <DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" sx={{ minWidth: 0 }}>
{event.link && ( {event.link && (
<Chip <Chip
component={Link} component={Link}
@@ -0,0 +1,58 @@
import { fireEvent, render, screen, within } from '@testing-library/react'
import ListView from './ListView.js'
import type { Event } from './types.js'
const makeEvent = (id: string, title: string, date: string, city = 'Ferrara'): Event => ({
id,
title,
date: new Date(date),
location: { name: 'Piazza', city, lat: 44, lng: 11 },
})
describe('Events list', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-10-19T12:00:00'))
})
afterEach(() => vi.useRealTimers())
it('separates events and sorts upcoming ascending and past descending', () => {
render(
<ListView
events={[
makeEvent('past-old', 'Past old', '2026-01-01'),
makeEvent('future-late', 'Future late', '2026-12-01'),
makeEvent('past-new', 'Past new', '2026-10-01'),
makeEvent('future-soon', 'Future soon', '2026-11-01'),
]}
onSelect={vi.fn()}
/>
)
const upcoming = within(screen.getByRole('region', { name: 'prossimi eventi' }))
const past = within(screen.getByRole('region', { name: 'eventi passati' }))
expect(upcoming.getAllByRole('heading', { level: 2 }).map(node => node.textContent)).toEqual(['Future soon', 'Future late'])
expect(past.getAllByRole('heading', { level: 2 }).map(node => node.textContent)).toEqual(['Past new', 'Past old'])
})
it('renders date ranges and invokes onSelect for the clicked card', () => {
const onSelect = vi.fn()
const event = { ...makeEvent('range', 'Long medieval event', '2026-11-01', 'San Martino'), endDate: new Date('2026-11-03') }
render(<ListView events={[event]} onSelect={onSelect} />)
expect(screen.getByText(/1-3 novembre 2026/)).toBeInTheDocument()
expect(screen.getByText('San Martino')).toBeInTheDocument()
fireEvent.click(screen.getByRole('heading', { name: 'Long medieval event' }))
expect(onSelect).toHaveBeenCalledWith(event)
})
it('uses a single fluid column on small screens', () => {
render(<ListView events={[makeEvent('one', 'A very long event title that must wrap', '2026-11-01')]} onSelect={vi.fn()} />)
const card = screen.getByRole('heading', { name: /very long event/ }).closest('.MuiCard-root')
expect(card).toHaveStyle({ width: '100%', minWidth: '0', boxSizing: 'border-box' })
expect(screen.getByRole('region', { name: 'prossimi eventi' })).toHaveStyle({ width: '100%', minWidth: '0' })
})
})
+33 -11
View File
@@ -31,7 +31,11 @@ const EventCard: React.FC<{ event: Event; onSelect: (event: Event) => void }> =
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
opacity: isUpcoming(event.date) ? 1 : 0.6, opacity: isUpcoming(event.date) ? 1 : 0.6,
width: theme => theme.breakpoints.values.lg * 0.3, // Cards must be allowed to shrink on phones. The previous fixed
// width (30% of the `lg` breakpoint) was wider than small screens.
width: '100%',
minWidth: 0,
boxSizing: 'border-box',
}}> }}>
<CardActionArea <CardActionArea
onClick={() => onSelect(event)} onClick={() => onSelect(event)}
@@ -39,27 +43,27 @@ const EventCard: React.FC<{ event: Event; onSelect: (event: Event) => void }> =
{event.image && <CardMedia component="img" height={160} image={event.image} alt={event.title} />} {event.image && <CardMedia component="img" height={160} image={event.image} alt={event.title} />}
<CardContent> <CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" mb={1}> <Stack direction="row" justifyContent="space-between" alignItems="flex-start" mb={1}>
<Typography variant="h6" component="h2" sx={{ flexGrow: 1, mr: 1 }}> <Typography variant="h6" component="h2" sx={{ flexGrow: 1, minWidth: 0, mr: 1, overflowWrap: 'anywhere' }}>
{event.title} {event.title}
</Typography> </Typography>
{!isUpcoming(event.date) && <Chip label="Passato" size="small" color="default" />} {!isUpcoming(event.date) && <Chip label="Passato" size="small" color="default" />}
</Stack> </Stack>
<Stack direction="row" alignItems="center" justifyContent="space-between"> <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ minWidth: 0 }}>
<div> <Box sx={{ minWidth: 0 }}>
<Stack direction="row" alignItems="center" spacing={0.5} mb={0.5}> <Stack direction="row" alignItems="center" spacing={0.5} mb={0.5}>
<CalendarMonthIcon fontSize="small" color="primary" /> <CalendarMonthIcon fontSize="small" color="primary" />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ overflowWrap: 'anywhere' }}>
{formatDateRange(event.date, event.endDate)} {formatDateRange(event.date, event.endDate)}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" alignItems="center" spacing={0.5}> <Stack direction="row" alignItems="center" spacing={0.5}>
<LocationOnIcon fontSize="small" color="primary" /> <LocationOnIcon fontSize="small" color="primary" />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ overflowWrap: 'anywhere' }}>
{event.location.city} {event.location.city}
</Typography> </Typography>
</Stack> </Stack>
</div> </Box>
<InfoOutline sx={{ color: 'text.secondary' }} /> <InfoOutline sx={{ color: 'text.secondary', flexShrink: 0, ml: 1 }} />
</Stack> </Stack>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
@@ -85,13 +89,22 @@ const ListView: React.FC<ListViewProps> = ({ events, onSelect }) => {
return groups return groups
}, [events]) }, [events])
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'flex-start', pb: 4 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 4, width: '100%', minWidth: 0, pb: 4 }}>
{eventGroups.upcoming.length > 0 && ( {eventGroups.upcoming.length > 0 && (
<> <>
<Typography variant="h5" component="h2" textAlign="center" mt={4} mb={2}> <Typography variant="h5" component="h2" textAlign="center" mt={4} mb={2}>
Prossimi eventi Prossimi eventi
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}> <Box
role="region"
aria-label="prossimi eventi"
sx={{
display: 'grid',
width: '100%',
minWidth: 0,
gridTemplateColumns: { xs: 'minmax(0, 1fr)', sm: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(3, minmax(0, 1fr))' },
gap: 3,
}}>
{eventGroups.upcoming.map(event => ( {eventGroups.upcoming.map(event => (
<EventCard key={event.id} event={event} onSelect={onSelect} /> <EventCard key={event.id} event={event} onSelect={onSelect} />
))} ))}
@@ -103,7 +116,16 @@ const ListView: React.FC<ListViewProps> = ({ events, onSelect }) => {
<Typography variant="h5" component="h3" textAlign="center" mt={4} mb={2}> <Typography variant="h5" component="h3" textAlign="center" mt={4} mb={2}>
Eventi passati Eventi passati
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}> <Box
role="region"
aria-label="eventi passati"
sx={{
display: 'grid',
width: '100%',
minWidth: 0,
gridTemplateColumns: { xs: 'minmax(0, 1fr)', sm: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(3, minmax(0, 1fr))' },
gap: 3,
}}>
{eventGroups.past.map(event => ( {eventGroups.past.map(event => (
<EventCard key={event.id} event={event} onSelect={onSelect} /> <EventCard key={event.id} event={event} onSelect={onSelect} />
))} ))}
+1 -1
View File
@@ -68,7 +68,7 @@ const MapView: React.FC<MapViewProps> = ({ events, onSelect }) => {
<MapContainer center={CENTER} zoom={6} scrollWheelZoom style={{ height: 560, width: '100%' }}> <MapContainer center={CENTER} zoom={6} scrollWheelZoom style={{ height: 560, width: '100%' }}>
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>' 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" url={`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=${import.meta.env.VITE_EV_CARTO_KEY}`}
/> />
{sorted.map((event, idx) => ( {sorted.map((event, idx) => (
<Marker <Marker
+21 -3
View File
@@ -105,7 +105,26 @@ export const events: Event[] = [
'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', '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', id: 'Autunno Ducale nelle Terre Estensi',
title: 'Autunno Ducale nelle Terre Estensi',
date: new Date('2026-10-24'),
endDate: new Date('2026-10-25'),
location: {
name: 'Ferrara',
city: 'Ferrara',
lat: 44.5,
lng: 11.37,
link: 'https://maps.app.goo.gl/TYLWP51PvjCjsxdw6',
},
link: 'https://www.prolocoferrara.org/category/eventi/autunno-ducale/',
image: 'https://www.prolocoferrara.org/wp-content/uploads/2026/07/locandina-AUTUNNO-DUCALE-2026.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: 'Usi & Costumi',
title: 'Usi & Costumi', title: 'Usi & Costumi',
date: new Date('2026-11-07'), date: new Date('2026-11-07'),
endDate: new Date('2026-11-08'), endDate: new Date('2026-11-08'),
@@ -117,8 +136,7 @@ export const events: Event[] = [
link: 'https://maps.app.goo.gl/q6MgoYrjm1uGxrrD8', link: 'https://maps.app.goo.gl/q6MgoYrjm1uGxrrD8',
}, },
link: 'https://www.usiecostumi.org/', link: 'https://www.usiecostumi.org/',
image: image: 'https://www.usiecostumi.org/wp-content/uploads/2024/04/usi-e-costumi-box.jpg',
'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: { details: {
description: 'La prima grande fiera internazionale, dedicata alla tradizione come strumento al servizio del turismo sostenibile.', description: 'La prima grande fiera internazionale, dedicata alla tradizione come strumento al servizio del turismo sostenibile.',
}, },
+4 -2
View File
@@ -22,8 +22,10 @@ const EventsPage = () => {
<Page <Page
title="Eventi" title="Eventi"
sx={{ sx={{
width: theme => theme.breakpoints.values.lg, width: '100%',
px: 'auto', maxWidth: theme => theme.breakpoints.values.lg,
boxSizing: 'border-box',
px: { xs: 2, sm: 3, md: 4 },
mx: 'auto', mx: 'auto',
}}> }}>
<Typography> <Typography>