diff --git a/.env.example b/.env.example index c0f1740..326f506 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX +VITE_BC_NAME=XXX XXXXXXX +VITE_BC_POSITION=XXXXXXXXXX +VITE_BC_VAT=ITXXXXXXXXXX +VITE_BC_PHONE=+39XXXXXXXXXX +VITE_BC_EMAIL=XXXX@XXXX.XX +VITE_BC_LINKEDIN=XXXXXXXXXX diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 66f27c5..4ee4c64 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,8 +37,27 @@ jobs: node-version: 'lts/*' cache: 'npm' - - name: Set VITE_GA_MEASUREMENT_ID - run: echo "VITE_GA_MEASUREMENT_ID=${{ secrets.VITE_GA_MEASUREMENT_ID }}" > .env + - name: Set environment variables + run: | + echo "VITE_GA_MEASUREMENT_ID=${{ secrets.VITE_GA_MEASUREMENT_ID }}" > .env + if [[ -n "${{ vars.VITE_BC_NAME }}" ]]; then + echo "VITE_BC_NAME=${{ vars.VITE_BC_NAME }}" >> .env + fi + if [[ -n "${{ vars.VITE_BC_POSITION }}" ]]; then + echo "VITE_BC_POSITION=${{ vars.VITE_BC_POSITION }}" >> .env + fi + if [[ -n "${{ vars.VITE_BC_VAT }}" ]]; then + echo "VITE_BC_VAT=${{ vars.VITE_BC_VAT }}" >> .env + fi + if [[ -n "${{ vars.VITE_BC_LINKEDIN }}" ]]; then + echo "VITE_BC_LINKEDIN=${{ vars.VITE_BC_LINKEDIN }}" >> .env + fi + if [[ -n "${{ secrets.VITE_BC_PHONE }}" ]]; then + echo "VITE_BC_PHONE=${{ secrets.VITE_BC_PHONE }}" >> .env + fi + if [[ -n "${{ secrets.VITE_BC_EMAIL }}" ]]; then + echo "VITE_BC_EMAIL=${{ secrets.VITE_BC_EMAIL }}" >> .env + fi - name: Install dependencies run: npm ci --omit=dev diff --git a/public/images/qr.svg b/public/images/qr.svg new file mode 100644 index 0000000..7470ba9 --- /dev/null +++ b/public/images/qr.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index a429bed..3b7fb4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ const ProjectsPage = React.lazy(() => import('components/pages/ProjectsPage/inde const CurriculumPage = React.lazy(() => import('components/pages/CurriculumPage/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')) const router = createBrowserRouter([ { @@ -57,6 +58,14 @@ const router = createBrowserRouter([ ), }, + { + path: 'card', + element: ( + }> + + + ), + }, ], }, ]) diff --git a/src/components/molecules/CookieConsent/index.tsx b/src/components/molecules/CookieConsent/index.tsx index 331dbed..bcd27c3 100644 --- a/src/components/molecules/CookieConsent/index.tsx +++ b/src/components/molecules/CookieConsent/index.tsx @@ -62,6 +62,9 @@ const CookieConsent = () => { mb: theme.spacing(1), p: theme.spacing(3), zIndex: 9999, + '@media print': { + display: 'none', + }, })}> diff --git a/src/components/molecules/CurriculumItem/CurriculumItem.tsx b/src/components/molecules/CurriculumItem/CurriculumItem.tsx index 2192e1f..237921d 100644 --- a/src/components/molecules/CurriculumItem/CurriculumItem.tsx +++ b/src/components/molecules/CurriculumItem/CurriculumItem.tsx @@ -32,10 +32,16 @@ const CurriculumItem = (props: CurriculumEntryProps) => { // '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { color: 'primary.dark' }, // use summary hover icon color }, '&.Mui-expanded': { + bgcolor: 'background.paper', // use summary hover background borderBottom: '1px solid', borderTop: '1px solid', - bgcolor: 'background.paper', // use summary hover background elevation: 1, + '@media print': { + bgcolor: 'background.default', + borderBottom: 0, + borderTop: 0, + elevation: 0, + }, // '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { color: 'primary.dark' }, // use summary hover icon color }, }}> @@ -48,6 +54,9 @@ const CurriculumItem = (props: CurriculumEntryProps) => { }, '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { transform: 'rotate(0deg)', + '@media print': { + transform: 'rotate(-90deg)', + }, }, '& .MuiAccordionSummary-content': { marginLeft: 2, diff --git a/src/components/molecules/Footer/index.tsx b/src/components/molecules/Footer/index.tsx index a34a5de..ec145fc 100644 --- a/src/components/molecules/Footer/index.tsx +++ b/src/components/molecules/Footer/index.tsx @@ -14,7 +14,17 @@ type FooterProps = { const Footer: React.FC = ({ showCookieConsent = true }) => { const year = new Date().getFullYear() return ( - + diff --git a/src/components/molecules/ProjectSpotlight/ProjectSpotlight.tsx b/src/components/molecules/ProjectSpotlight/ProjectSpotlight.tsx index 05d2d3c..e31aebb 100644 --- a/src/components/molecules/ProjectSpotlight/ProjectSpotlight.tsx +++ b/src/components/molecules/ProjectSpotlight/ProjectSpotlight.tsx @@ -1,6 +1,6 @@ import React from 'react' import { ParallaxBanner, ParallaxProvider } from 'react-scroll-parallax' -import { Box, Grid, IconButton, Typography, useTheme } from '@mui/material' +import { Box, Grid, IconButton, Typography } from '@mui/material' import { ExpandMore } from '@mui/icons-material' import { useViewport } from 'components/utils/viewport/index.js' @@ -125,79 +125,91 @@ const Description = (props: Props) => { } } -const DesktopComponent = React.forwardRef((props, ref) => { - return ( -
- - ((props, ref) => { + const { width } = useViewport() - height: '100vh', - overflow: 'hidden', - }}> - - {props.scrollTarget && ( - (props.scrollTarget as React.RefObject).current?.scrollIntoView()} - sx={{ position: 'absolute', p: 1, bottom: 2, left: '50%', transform: 'translate(-50%, 0)' }}> - - - )} - - -
- ) -}) - -const MobileComponent = React.forwardRef((props, ref) => { return ( - - + ({ - mt: theme.spacing(-1.05), - borderTopStyle: 'solid', - borderTopWidth: theme.spacing(0.5), - borderTopColor: theme.palette.primary.main, - p: `${theme.spacing(7)} ${theme.spacing(5)} ${theme.spacing(5.5)} ${theme.spacing(5)}`, + display: width < theme.breakpoints.values.md ? 'none' : 'inline', + '@media print': { display: 'none' }, })}> - {props.title} - {props.caption} - {props.children} + + + + {props.scrollTarget && ( + (props.scrollTarget as React.RefObject).current?.scrollIntoView()} + sx={{ position: 'absolute', p: 1, bottom: 2, left: '50%', transform: 'translate(-50%, 0)' }}> + + + )} + + + + ({ + display: width >= theme.breakpoints.values.md ? 'none' : 'inline', + '@media print': { + display: 'inline', + height: '100%', + }, + })}> + + ({ + mt: theme.spacing(-1.05), + borderTopStyle: 'solid', + borderTopWidth: theme.spacing(0.5), + borderTopColor: theme.palette.primary.main, + p: `${theme.spacing(7)} ${theme.spacing(5)} ${theme.spacing(5.5)} ${theme.spacing(5)}`, + '@media print': { + backgroundColor: 'background.default', + }, + })}> + {props.title} + {props.caption} + {props.children} + ) }) -const ProjectSpotlight = React.forwardRef((props, ref) => { - const { width } = useViewport() - const theme = useTheme() - - return width < theme.breakpoints.values.md ? : -}) - export default ProjectSpotlight diff --git a/src/components/molecules/Toolbar/index.tsx b/src/components/molecules/Toolbar/index.tsx index aeaeeb5..aeb701d 100644 --- a/src/components/molecules/Toolbar/index.tsx +++ b/src/components/molecules/Toolbar/index.tsx @@ -15,22 +15,30 @@ import { Stack, Toolbar, Typography, + TypographyProps, styled, useScrollTrigger, useTheme, } from '@mui/material' import MenuIcon from '@mui/icons-material/Menu' -const AppBarButton = styled(Button)(({ theme }) => ({ - textTransform: 'none', - color: theme.palette.mode === 'dark' ? 'inherit' : 'text.secondary', - ':hover': { - backgroundColor: 'transparent', - ...(theme.palette.mode === 'dark' && { +const TypographyLink = styled(Typography)(({ theme }) => ({ + role: 'link', + textDecoration: 'none', + ...(theme.palette.mode === 'dark' && { + color: 'inherit', + ':hover': { color: theme.palette.primary.main, - }), - }, + }, + }), + ...(theme.palette.mode === 'light' && { + color: theme.palette.text.primary, + ':hover': { + color: theme.palette.primary.dark, + }, + }), })) +const AppBarLink = (props: TypographyProps & { to: string }) => const DrawerButton = styled(ListItemButton)(({ theme }) => ({ textTransform: 'none', color: 'inherit', @@ -102,6 +110,7 @@ const CustomToolbar: React.FC = ({ drawerWidth = 240, window {...props} sx={{ backgroundColor: trigger ? 'background.paper' : 'transparent', + '@media print': { backgroundColor: 'background.default' }, ...(theme.palette.mode === 'light' && { backgroundColor: 'hsla(0, 0%, 100%, 0.75)', backdropFilter: 'blur(8px)', @@ -115,6 +124,7 @@ const CustomToolbar: React.FC = ({ drawerWidth = 240, window theme.breakpoints.values.lg, mx: 'auto' }, }}> @@ -123,58 +133,47 @@ const CustomToolbar: React.FC = ({ drawerWidth = 240, window aria-label="open drawer" edge="start" onClick={handleDrawerToggle} - sx={{ mr: 2, display: { md: 'none' }, position: 'absolute', backgroundColor: 'primary' }}> + sx={{ + mr: 2, + display: { md: 'none' }, + position: 'absolute', + backgroundColor: 'primary', + '@media print': { display: 'none' }, + }}> - ({ - color: theme.palette.text.primary, [theme.breakpoints.down('md')]: { mx: 'auto' }, [theme.breakpoints.up('md')]: { mr: 'auto' }, })}> - Michele Tartari - - - - Home - - - Projects - - - Curriculum - - - + + {drawer} + ) } diff --git a/src/components/pages/BusinessCardPage/BusinessCardPage.test.tsx b/src/components/pages/BusinessCardPage/BusinessCardPage.test.tsx new file mode 100644 index 0000000..dbbf4be --- /dev/null +++ b/src/components/pages/BusinessCardPage/BusinessCardPage.test.tsx @@ -0,0 +1,82 @@ +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +// 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 BusinessCardPage from './index.js' + +vi.mock('./envVars.js', () => ({ + NAME: 'Test Name', + POSITION: 'Test Position', + VAT: 'Test VAT', + PHONE: '+123456789', + EMAIL: 'test@example.com', + LINKEDIN: 'testlinkedin', +})) + +// Mock window.print +const mockPrint = vi.fn() +window.print = mockPrint + +// Mock the analytics module +vi.mock('use-analytics', () => ({ + useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }), +})) + +describe('BusinessCardPage', () => { + beforeEach(() => { + mockPrint.mockClear() + }) + + it('renders correctly with environment variables', () => { + render() + + // Name appears in uppercase in the first card + expect(screen.getAllByText('Test Name')).toHaveLength(2) + expect(screen.getByText('Test Position')).toBeInTheDocument() + + // Check if values are rendered correctly in the second card + expect(screen.getByText('Test VAT')).toBeInTheDocument() + expect(screen.getByText('+123456789')).toBeInTheDocument() + expect(screen.getByText('test@example.com')).toBeInTheDocument() + expect(screen.getByText('www.m-tartari.eu')).toBeInTheDocument() + expect(screen.getByText('testlinkedin')).toBeInTheDocument() + }) + + it('has correct links with proper href attributes', () => { + render() + + // Check phone link + const phoneLink = screen.getByText('+123456789').closest('a') + expect(phoneLink).toHaveAttribute('href', 'tel:+123456789') + + // Check email link + const emailLink = screen.getByText('test@example.com').closest('a') + expect(emailLink).toHaveAttribute('href', 'mailto:test@example.com') + + // Check website link + const websiteLink = screen.getByText('www.m-tartari.eu').closest('a') + expect(websiteLink).toHaveAttribute('href', 'https://www.m-tartari.eu') + + // Check LinkedIn link + const linkedinLink = screen.getByText('testlinkedin').closest('a') + expect(linkedinLink).toHaveAttribute('href', 'https://www.linkedin.com/in/testlinkedin/') + }) + + it('calls window.print when print button is clicked', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByText('Print Business Card')) + + expect(mockPrint).toHaveBeenCalledTimes(1) + }) + + it('renders correct number of business cards', () => { + render() + + const businessCards = screen.getAllByRole('img', { name: /logo|qr code/i }) + expect(businessCards).toHaveLength(2) + }) +}) diff --git a/src/components/pages/BusinessCardPage/envVars.ts b/src/components/pages/BusinessCardPage/envVars.ts new file mode 100644 index 0000000..76484ac --- /dev/null +++ b/src/components/pages/BusinessCardPage/envVars.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +export const NAME = import.meta.env.VITE_BC_NAME || '[FULL NAME]' +export const POSITION = import.meta.env.VITE_BC_POSITION || '[POSITION]' +export const VAT = import.meta.env.VITE_BC_VAT || '[VAT NUMBER]' +export const PHONE = import.meta.env.VITE_BC_PHONE || '[Redacted for privacy]' +export const EMAIL = import.meta.env.VITE_BC_EMAIL || '[Redacted for privacy]' +export const LINKEDIN = import.meta.env.VITE_BC_LINKEDIN || '[Redacted for privacy]' diff --git a/src/components/pages/BusinessCardPage/index.css b/src/components/pages/BusinessCardPage/index.css new file mode 100644 index 0000000..1fb5fd5 --- /dev/null +++ b/src/components/pages/BusinessCardPage/index.css @@ -0,0 +1,31 @@ +/* Print styles for business card */ +@media print { + body * { + visibility: hidden; + } + + #main, + #page-content { + margin-bottom: 0; + margin-top: 0; + min-height: 0; + } + + #footer { + display: none; + } + + .print-business-card, + .print-business-card * { + visibility: visible; + } + + .print-business-card { + width: 85mm; + height: 55mm; + } + + @page { + size: 85mm 55mm; + } +} diff --git a/src/components/pages/BusinessCardPage/index.tsx b/src/components/pages/BusinessCardPage/index.tsx new file mode 100644 index 0000000..84bac9a --- /dev/null +++ b/src/components/pages/BusinessCardPage/index.tsx @@ -0,0 +1,185 @@ +import { Box, Typography, Stack, Button, Paper, Link, TypographyProps, SvgIcon, PaperProps, ThemeProvider, createTheme } from '@mui/material' +import { Email, LinkedIn, Print, PhoneInTalk, Public } from '@mui/icons-material' + +import { Page } from 'components/templates/index.js' +import typography from 'style/typography/index.js' +import palette from 'style/palette/index.js' +import * as env from './envVars.js' +import './index.css' + +const TypographyLink = ({ icon: Icon, children, ...props }: TypographyProps) => ( + + + + {children} + + +) + +const theme = { + light: createTheme({ + typography: typography, + palette: palette.light, + }), + dark: createTheme({ + typography: typography, + palette: palette.dark, + }), +} + +type BusinessCardProps = { + mode?: 'light' | 'dark' + src: string + alt?: string +} & PaperProps +const BusinessCard = ({ mode = 'dark', src, alt, sx, ...props }: BusinessCardProps) => ( + + + + + {/* Right side with content */} + + {props.children} + + + +) + +/** + * BusinessCardPage component - A digital business card + * Designed to be printable as a standard business card (3.5 x 2 inches) + */ +const BusinessCardPage = () => { + return ( + + + + + {env.NAME} + + + {env.POSITION} + + + + + + + {env.NAME} + + + {env.VAT} + + + {env.PHONE} + + + {env.EMAIL} + + + www.m-tartari.eu + + + {env.LINKEDIN} + + + + + + + + + + ) +} + +export default BusinessCardPage diff --git a/src/components/pages/CurriculumPage/CurriculumPage.tsx b/src/components/pages/CurriculumPage/CurriculumPage.tsx index 793135d..a7da22c 100644 --- a/src/components/pages/CurriculumPage/CurriculumPage.tsx +++ b/src/components/pages/CurriculumPage/CurriculumPage.tsx @@ -1,4 +1,4 @@ -import { Box, Divider, Link, Stack, Table, TableCell, TableRow, Typography } from '@mui/material' +import { Box, Divider, Link, Stack, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material' import { Award, ExternalIconLink, Extracurricular, Publication, SectionHeader, SectionSubheader } from 'components/atoms/index.js' import { CurriculumItem } from 'components/molecules/index.js' @@ -12,20 +12,18 @@ const Curriculum = () => { {/* id="selfemployed-software" */} - -
    -
  • - Design and development of customized graphical interfaces and software components to facilitate the use of robots and industrial - machines. -
  • -
  • Production and maintenance of revisions of the documentation.
  • -
  • Providing technical assistance for R&D activities.
  • -
+ +
  • + Design and development of customized graphical interfaces and software components to facilitate the use of robots and industrial + machines. +
  • +
  • Production and maintenance of revisions of the documentation.
  • +
  • Providing technical assistance for R&D activities.
  • {/* id="MinHub-lead" */} - + Co-founded{' '} { {/* id="IIT-software" */} - + Worked in{' '} Alberobotics @@ -76,7 +74,7 @@ const Curriculum = () => { {/* id="IIT-embedded" */} - + Worked in{' '} Alberobotics @@ -104,7 +102,7 @@ const Curriculum = () => { dates="01/19 - 06/19" company="Warsaw University of Technology - Division of Theory of Machines and Robots" location="Warsaw, Poland"> - + My main duties included:
    • Developing a voice interface and some user detection strategies for low-power robotic systems using ROS.
    • @@ -115,7 +113,7 @@ const Curriculum = () => { {/* id="WUT-intern" */} - + My main duties included:
      • Developing a setup to control a shape memory alloy material using an embedded PC (a Beagle Bone Black).
      • @@ -157,26 +155,24 @@ const Curriculum = () => { // (grade: 5/5,summa cum laude) location="Warsaw, Poland" dates="09/18 - 06/19"> - -
          -
        • Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.
        • -
        • Relevant courses: Research Methodology, Bio-Robotics, Dynamics of Multi-Body Systems, Advanced Mechanical Design.
        • -
        • Other relevant courses: 2 exams of Polish language and 1 of Polish culture.
        • -
        • - Thesis:{' '} - - Voice Controlled Mobile Robot for Indoor Applications - {' '} - (research publication available{' '} - - here - - ). -
        • -
        + +
      • Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.
      • +
      • Relevant courses: Research Methodology, Bio-Robotics, Dynamics of Multi-Body Systems, Advanced Mechanical Design.
      • +
      • Other relevant courses: 2 exams of Polish language and 1 of Polish culture.
      • +
      • + Thesis:{' '} + + Voice Controlled Mobile Robot for Indoor Applications + {' '} + (research publication available{' '} + + here + + ). +
      • {/* id="ECN" */} @@ -185,25 +181,23 @@ const Curriculum = () => { company="Ecole Centrale de Nantes" location="Nantes, France" dates="09/17 - 06/18"> - -
          -
        • Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.
        • -
        • - Relevant courses: Mobile Robotics, Non-linear Control Theory, Software Architecture for Robotics, Artificial Intelligence, Computer - Vision, Real Time Systems, Mechanical Design Methods in Robotics, Optimization Techniques. -
        • -
        • Other relevant courses: 2 exams of French language.
        • -
        • - Project:{' '} - - Recharging electric cars with a mobile robot - - . -
        • -
        + +
      • Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.
      • +
      • + Relevant courses: Mobile Robotics, Non-linear Control Theory, Software Architecture for Robotics, Artificial Intelligence, Computer + Vision, Real Time Systems, Mechanical Design Methods in Robotics, Optimization Techniques. +
      • +
      • Other relevant courses: 2 exams of French language.
      • +
      • + Project:{' '} + + Recharging electric cars with a mobile robot + + . +
      • @@ -227,18 +221,16 @@ const Curriculum = () => { // (grade: 5/5) location="Shanghai, China" dates="09/14 - 06/17"> - -
          -
        • Relevant courses: Automation Controls, Electrical Circuit, Logical Design of Digital Systems.
        • -
        • Other relevant courses: 2 Exams about Chinese Language and 3 Exams about Chinese Culture.
        • -
        • - Thesis:{' '} - - Solar Panel Fault Detection Method Based on Infrared Image Processing - - . -
        • -
        + +
      • Relevant courses: Automation Controls, Electrical Circuit, Logical Design of Digital Systems.
      • +
      • Other relevant courses: 2 Exams about Chinese Language and 3 Exams about Chinese Culture.
      • +
      • + Thesis:{' '} + + Solar Panel Fault Detection Method Based on Infrared Image Processing + + . +
      • {/* id="UniBo" */} @@ -248,19 +240,17 @@ const Curriculum = () => { location="Bologna, Italy" //(grade: 102/110) dates="09/13 - 12/16"> - -
          -
        • Scholarship issued for Almatong Project student based on academic merits and offered to top applicants only.
        • -
        • Relevant courses: Foundation of Industrial Robotics, Automatic Machines, Computer Architectures.
        • -
        • Other relevant courses: 2 exams of French language.
        • -
        • - Thesis:{' '} - - Design of a Testing Machine for Flexure Hinges - - . -
        • -
        + +
      • Scholarship issued for Almatong Project student based on academic merits and offered to top applicants only.
      • +
      • Relevant courses: Foundation of Industrial Robotics, Automatic Machines, Computer Architectures.
      • +
      • Other relevant courses: 2 exams of French language.
      • +
      • + Thesis:{' '} + + Design of a Testing Machine for Flexure Hinges + + . +
      • @@ -305,50 +295,52 @@ const Curriculum = () => { Skills - - - - Languages - - - Italian (Native Speaker), English (Proficient, IELTS 7.5), French (Good), Chinese (Basic, HSK3) - + + + + + Languages + + + Italian (Native Speaker), English (Proficient, IELTS 7.5), French (Good), Chinese (Basic, HSK3) + - - - - Digital - - - - Languages: TypeScript/Javascript (Proficient), Python (Proficient), Bash (Good), C++ (Good), C (Fair) - - - - - Softwares: git (Proficient), Matlab (Good), LaTeX (Good), CATIA (Fair) - - - - - Middleware: Robotic Operative System (ROS, Good) - - - - - OS Used: Linux, Microsoft - - - - - - Softskills - - - - Adaptability, Communication, Creativity, Critical Thinking, Problem-Solving, Self-Organization, Teamworking - - + + + + Digital + + + + Languages: TypeScript/Javascript (Proficient), Python (Proficient), Bash (Good), C++ (Good), C (Fair) + + + + + Softwares: git (Proficient), Matlab (Good), LaTeX (Good), CATIA (Fair) + + + + + Middleware: Robotic Operative System (ROS, Good) + + + + + OS Used: Linux, Microsoft + + + + + + Softskills + + + + Adaptability, Communication, Creativity, Critical Thinking, Problem-Solving, Self-Organization, Teamworking + + +
        {/* Accomplishments */} diff --git a/src/components/pages/ErrorPage/ErrorPage.test.tsx b/src/components/pages/ErrorPage/ErrorPage.test.tsx index ff6dac9..b4825b1 100644 --- a/src/components/pages/ErrorPage/ErrorPage.test.tsx +++ b/src/components/pages/ErrorPage/ErrorPage.test.tsx @@ -1,4 +1,4 @@ -import { screen, cleanup, render } from '@testing-library/react' +import { screen, render } from '@testing-library/react' import { RouterProvider, createMemoryRouter } from 'react-router-dom' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { useAnalytics } from 'use-analytics' @@ -10,19 +10,14 @@ vi.mock('use-analytics', () => ({ useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }), })) -// Tests -describe('Renders ErrorPage correctly', async () => { - /** - * Resets all renders after each test - */ - afterEach(() => { - cleanup() - }) +// Suppress console.error messages during tests +const originalConsoleError = console.error +console.error = vi.fn() - /** - * Passes - shows title correctly - */ +describe('Renders ErrorPage correctly', async () => { it('Should render the error page correctly', () => { + console.error = vi.fn() + const badRoute = '/some/bad/route' const router = createMemoryRouter([{ path: '/', element:
        , errorElement: }], { initialEntries: [badRoute] }) @@ -33,3 +28,5 @@ describe('Renders ErrorPage correctly', async () => { expect(screen.getByText(`404 Error: No route matches URL "${badRoute}"`)).toBeInTheDocument() }) }) + +console.error = originalConsoleError diff --git a/src/components/pages/MainPage/MainPage.test.tsx b/src/components/pages/MainPage/MainPage.test.tsx index ac87bdb..89cd7d1 100644 --- a/src/components/pages/MainPage/MainPage.test.tsx +++ b/src/components/pages/MainPage/MainPage.test.tsx @@ -18,25 +18,10 @@ describe('Main page', () => { // Make sure the page title is correct expect(screen.getByText('Robotics Engineer')).toBeInTheDocument() - // Make sure the project spotlights are present - expect(screen.getByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toBeInTheDocument() - expect(screen.getByText("Revamping HHCM's Modular: Swift Generation of Modular Robot Models")).toBeInTheDocument() - expect(screen.getByText('Automated Actuators and Robotic Modules Calibration')).toBeInTheDocument() - expect(screen.getByText('Voice Controlled Mobile Robot for Indoor Applications')).toBeInTheDocument() - }) - it('Should render the page correctly on mobile', async () => { - // Set the window size to mobile (Galaxy Note 20 Android 11) - window.innerWidth = 412 - window.innerHeight = 915 - await render() - - // Make sure the page title is correct - expect(screen.getByText('Robotics Engineer')).toBeInTheDocument() - - // Make sure the project spotlights are present - expect(screen.getByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toBeInTheDocument() - expect(screen.getByText("Revamping HHCM's Modular: Swift Generation of Modular Robot Models")).toBeInTheDocument() - expect(screen.getByText('Automated Actuators and Robotic Modules Calibration')).toBeInTheDocument() - expect(screen.getByText('Voice Controlled Mobile Robot for Indoor Applications')).toBeInTheDocument() + // Make sure the project spotlights are present for both desktop and mobile + expect(screen.getAllByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toHaveLength(2) + expect(screen.getAllByText("Revamping HHCM's Modular: Swift Generation of Modular Robot Models")).toHaveLength(2) + expect(screen.getAllByText('Automated Actuators and Robotic Modules Calibration')).toHaveLength(2) + expect(screen.getAllByText('Voice Controlled Mobile Robot for Indoor Applications')).toHaveLength(2) }) }) diff --git a/src/components/pages/index.ts b/src/components/pages/index.ts index 10e8aab..a296ebb 100644 --- a/src/components/pages/index.ts +++ b/src/components/pages/index.ts @@ -2,5 +2,6 @@ export { default as MainPage } from './MainPage' export { default as ContactPage } from './ContactPage' export { default as CurriculumPage } from './CurriculumPage' export { default as ProjectsPage } from './ProjectsPage' +export { default as BusinessCardPage } from './BusinessCardPage' export { default as ErrorPage } from './ErrorPage' diff --git a/src/components/templates/Page/Page.tsx b/src/components/templates/Page/Page.tsx index 9508681..b4d776a 100644 --- a/src/components/templates/Page/Page.tsx +++ b/src/components/templates/Page/Page.tsx @@ -19,11 +19,17 @@ const Page: React.FC = ({ title, children, showCookieConsent, ...prop }, [title]) return ( - + {title && ( <> = ({ title, children, showCookieConsent, ...prop )} { cleanup() }) +/** + * Mocks the window.scrollTo method (called on page change) + */ +vi.stubGlobal('scrollTo', vi.fn()) + /** * Mocks the ResizeObserver API (used by ParallaxProvider) */ diff --git a/src/style/palette/index.ts b/src/style/palette/index.ts new file mode 100644 index 0000000..7dae129 --- /dev/null +++ b/src/style/palette/index.ts @@ -0,0 +1,31 @@ +import { PaletteOptions } from '@mui/material' + +export const dark: PaletteOptions = { + mode: 'dark', + primary: { + main: '#e44c65', // Rouge Red (similar) + // main:'#df4661', // Pantone PMS 198 C + }, + secondary: { + main: '#3f51b5', // Early Spring Night (similar) + // main: '#3a5dae', // Pantone 7455 C Color + }, + background: { + default: '#101018', + paper: '#1b1d27', + }, +} +export const light: PaletteOptions = { + primary: { + main: '#AC1D1B', // Casual Red + // main: '#AF231C' // Pantone 2350 C + // main: '#AB2328' // Pantone 7621 C + }, + secondary: { + main: '#006269', // Pantone 7715 C + }, + background: { default: '#fafafa', paper: '#eeeeee' }, // Light background + text: { secondary: '#666' }, // Dark text for readability +} + +export default { dark, light } diff --git a/src/style/theme.ts b/src/style/theme.ts index cc58e6c..63435e8 100644 --- a/src/style/theme.ts +++ b/src/style/theme.ts @@ -1,58 +1,24 @@ /* istanbul ignore file */ import { createTheme, responsiveFontSizes } from '@mui/material/styles' -// A custom theme for this app -let theme = createTheme({ - typography: { - fontFamily: 'Inter, sans-serif', // Base font for headings - h1: { - fontFamily: 'Oswald, sans-serif', - textTransform: 'uppercase', - fontWeight: 700, - }, - h2: { - fontFamily: 'Oswald, sans-serif', - textTransform: 'uppercase', - fontWeight: 700, - }, - h3: { - fontFamily: 'Oswald, sans-serif', - fontWeight: 700, - }, - h4: { - fontFamily: 'Oswald, sans-serif', - fontWeight: 700, - }, - }, +import typography from './typography/index.js' +import palette from './palette/index.js' + +let theme = createTheme({ + typography: typography, colorSchemes: { - dark: { - palette: { - primary: { - main: '#e44c65', // Rouge Red (similar) - // main:'#df4661', // Pantone PMS 198 C + light: { palette: palette.light }, + dark: { palette: palette.dark }, + }, + components: { + MuiCollapse: { + styleOverrides: { + hidden: { + '@media print': { + visibility: 'visible', + height: 'auto', + }, }, - secondary: { - main: '#3f51b5', // Early Spring Night (similar) - // main: '#3a5dae', // Pantone 7455 C Color - }, - background: { - default: '#101018', - paper: '#1b1d27', - }, - }, - }, - light: { - palette: { - primary: { - main: '#AC1D1B', // Casual Red - // main: '#AF231C' // Pantone 2350 C - // main: '#AB2328' // Pantone 7621 C - }, - secondary: { - main: '#006269', // Pantone 7715 C - }, - background: { default: '#fafafa', paper: '#eeeeee' }, // Light background - text: { secondary: '#666' }, // Dark text for readability }, }, }, diff --git a/src/style/typography/index.ts b/src/style/typography/index.ts new file mode 100644 index 0000000..231559e --- /dev/null +++ b/src/style/typography/index.ts @@ -0,0 +1,26 @@ +import { TypographyVariantsOptions } from '@mui/material' + +const typography: TypographyVariantsOptions = { + fontFamily: 'Inter, sans-serif', // Base font for headings + + h1: { + fontFamily: 'Oswald, sans-serif', + textTransform: 'uppercase', + fontWeight: 700, + }, + h2: { + fontFamily: 'Oswald, sans-serif', + textTransform: 'uppercase', + fontWeight: 700, + }, + h3: { + fontFamily: 'Oswald, sans-serif', + fontWeight: 700, + }, + h4: { + fontFamily: 'Oswald, sans-serif', + fontWeight: 700, + }, +} + +export default typography diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index c8142b1..500b864 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,12 @@ interface ImportMetaEnv { readonly VITE_GA_MEASUREMENT_ID: string + readonly VITE_BC_NAME: string + readonly VITE_BC_POSITION: string + readonly VITE_BC_VAT: string + readonly VITE_BC_PHONE: string + readonly VITE_BC_EMAIL: string + readonly VITE_BC_LINKEDIN: string } interface ImportMeta {