✨ feat(people): add people page

This commit is contained in:
2026-01-27 16:01:12 +01:00
parent d356ffe31c
commit 2c546e766b
17 changed files with 165 additions and 3 deletions
+9
View File
@@ -6,6 +6,7 @@ import RootLayout from 'components/templates/RouteTracker/index.js'
import Loading from 'components/molecules/LoadingScreen/index.js'
const MainPage = React.lazy(() => import('components/pages/MainPage/index.js'))
const PeoplePage = React.lazy(() => import('components/pages/PeoplePage/index.js'))
const ContactPage = React.lazy(() => import('components/pages/ContactPage/index.js'))
const PrivacyPage = React.lazy(() => import('components/pages/PrivacyPage/index.js'))
const BusinessCardPage = React.lazy(() => import('components/pages/BusinessCardPage/index.js'))
@@ -24,6 +25,14 @@ const router = createBrowserRouter([
</React.Suspense>
),
},
{
path: 'people',
element: (
<React.Suspense fallback={<Loading />}>
<PeoplePage />
</React.Suspense>
),
},
{
path: 'contacts',
element: (
+3 -3
View File
@@ -1,4 +1,4 @@
import { Email, LinkedIn, GitHub, Description, OndemandVideo, School, OpenInNew, OpenInBrowser } from '@mui/icons-material'
import { Description, Email, Facebook, Instagram, OndemandVideo, School, OpenInNew, OpenInBrowser } from '@mui/icons-material'
import { Badge, Link, LinkProps, SvgIcon } from '@mui/material'
interface IconLinkProps extends LinkProps {
@@ -55,8 +55,8 @@ const IconWrapper: React.FC<IconWrapperProps> = ({ index, fontSize, Icon, sx, ..
export const DescriptionIconLink = (props: IconLinkProps) => <IconWrapper aria-label="Doc-button" Icon={Description} {...props} />
export const EmailIconLink = (props: IconLinkProps) => <IconWrapper aria-label="Email-button" Icon={Email} {...props} />
export const GitHubIconLink = (props: IconLinkProps) => <IconWrapper aria-label="GitHub-button" Icon={GitHub} {...props} />
export const LinkedInIconLink = (props: IconLinkProps) => <IconWrapper aria-label="LinkedIn-button" Icon={LinkedIn} {...props} />
export const InstagramIconLink = (props: IconLinkProps) => <IconWrapper aria-label="Instagram-button" Icon={Instagram} {...props} />
export const FacebookIconLink = (props: IconLinkProps) => <IconWrapper aria-label="LinkedIn-button" Icon={Facebook} {...props} />
export const AcademyIconLink = (props: IconLinkProps) => <IconWrapper aria-label="Academy-button" Icon={School} {...props} />
export const VideoIconLink = (props: IconLinkProps) => <IconWrapper aria-label="Video-button" Icon={OndemandVideo} {...props} />
export const ExternalIconLink = (props: IconLinkProps) => <IconWrapper aria-label="External-link-button" Icon={OpenInNew} {...props} />
@@ -0,0 +1,28 @@
import { Card, CardContent, CardMedia, CardProps, Typography } from '@mui/material'
import { FC } from 'react'
export interface EmplyeeCardProps extends CardProps {
name: string
image: string
position?: string
}
export const EmplyeeCard: FC<EmplyeeCardProps> = ({ name, position, image, sx, ...props }) => {
return (
<Card sx={{ flexShrink: 0, width: 270, height: 360, ...sx }} {...props}>
<CardMedia component="img" sx={{ minHeight: '140' }} image={image} alt={`${name} image`} />
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{name}
</Typography>
{position && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{position}
</Typography>
)}
</CardContent>
</Card>
)
}
export default EmplyeeCard
@@ -0,0 +1,56 @@
// src/components/StackedDeck.tsx
import { FC, useEffect, useState } from 'react'
import { Box } from '@mui/material'
import { EmplyeeCard, EmplyeeCardProps } from 'components/molecules/EmplyeeCard/index.js'
interface Props {
items: EmplyeeCardProps[]
// Visual offsets
offsetStep?: number // px shift per card
maxVisible?: number // how many cards we actually render (performance)
}
const EmplyeeStack: FC<Props> = ({
offsetStep = 10, // px shift per card
maxVisible = 6, // how many cards we actually render (performance)
items: initialItems,
}) => {
const [items, setItems] = useState(initialItems)
useEffect(() => {
setItems(initialItems)
}, [initialItems])
const handleTopClick = () => {
if (items.length) {
setItems([...items.slice(1), items[0]])
}
}
return (
<Box
onClick={handleTopClick}
sx={{
position: 'relative',
width: 270 + offsetStep * (initialItems.length - 1),
height: 360 + offsetStep * (initialItems.length - 1),
}}>
{items.slice(0, maxVisible).map((props, idx) => (
<EmplyeeCard
{...props}
key={idx}
sx={{
cursor: 'pointer',
position: 'absolute',
top: idx * offsetStep,
left: idx * offsetStep,
zIndex: initialItems.length - idx, // top card gets highest z-index
}}
/>
))}
</Box>
)
}
export default EmplyeeStack
@@ -0,0 +1,28 @@
import { screen } from '@testing-library/react'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useAnalytics } from 'use-analytics'
import { renderWithRouter as render } from 'components/utils/tests/index.js'
import ProjectsPage from '.'
// Mock the analytics module
vi.mock('use-analytics', () => ({
useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }),
}))
vi.stubGlobal('open', vi.fn())
describe('ProjectsPage component', () => {
test('renders without crashing and displays the correct project titles', () => {
render(<ProjectsPage />)
// check the section titles
expect(screen.getByText('Robotics')).toBeInTheDocument()
expect(screen.getByText('Engineering')).toBeInTheDocument()
expect(screen.getByText('Hobbies')).toBeInTheDocument()
// check some of the projects
expect(screen.getByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toBeInTheDocument()
expect(screen.getByText("Revamping HHCM's Modular: Swift Generation and Control of Modular Robot Models")).toBeInTheDocument()
expect(screen.getByText("Scuola d'arme")).toBeInTheDocument()
})
})
+40
View File
@@ -0,0 +1,40 @@
import { Box, Typography } from '@mui/material'
import EmplyeeCard from 'components/molecules/EmplyeeCard/index.js'
import { Page } from 'components/templates/index.js'
import EmplyeeStack from '../../molecules/EmplyeeStack/index.js'
const ProjectsPage = () => {
return (
<Page title="Personaggi">
<Typography pb={2}>
Tutti i Personaggi storici della Compagnia sono realmente vissuti ed esistono attendibili fonti bibliografiche. Il gruppo è suddiviso per
classi sociali: Uomini d'arme, Artigiani, Nobili Cavalieri, Nobildonne, uomini e donne del popolo; Ogni Personaggio, il suo ruolo e il suo
comportamento sono fondamentali alla ricostruzione di uno spaccato della vita di un campo d'arme del tardo XIV Secolo.
</Typography>
<Box display="flex" gap={1} flexWrap="wrap" justifyContent="center">
<EmplyeeStack
items={[
{ name: 'Alberto V', image: 'images/people/albertoV.jpg', position: 'Marchese' },
{ name: 'Bartolomeo Contrari', image: 'images/people/bartolomeo.jpg', position: 'Nobile' },
{ name: 'Zan Biancolin', image: 'images/people/zanbiancolin.jpg', position: 'Popolano' },
]}
/>
<EmplyeeStack
items={[
{ name: 'Niccolò degli Obizzi', image: 'images/people/niccolo_degli_obizzi.jpg', position: 'Nobile' },
{ name: 'Ferantino di Zampalocha', image: 'images/people/ferantino_di_zampalocha.jpg', position: 'Fante' },
]}
/>
<EmplyeeCard name="Galacino Galaci" image="images/people/galacino_galaci.jpg" position="Fante" />
<EmplyeeCard name="Zanino il Beccaio" image="images/people/zannino_il_beccaio.jpg" position="Fante" />
<EmplyeeCard name="Gamberino di Bergantino" image="images/people/gamberino_di_bergantino.jpg" position="Fante" />
<EmplyeeCard name="Albertino della Muzzina" image="images/people/albertino_della_muzzina.jpg" position="Fante" />
<EmplyeeCard name="Ghisola Casale da Castrocaro" image="images/people/ghisola_da_castrocaro.jpg" position="Popolano" />
</Box>
</Page>
)
}
export default ProjectsPage
+1
View File
@@ -1,5 +1,6 @@
export { default as MainPage } from './MainPage'
export { default as ContactPage } from './ContactPage'
export { default as ProjectsPage } from './PeoplePage'
export { default as BusinessCardPage } from './BusinessCardPage'
export { default as ErrorPage } from './ErrorPage'