Merge pull request #530 from m-tartari/feat/business-card

✨ feat(card): add Business Card page
This commit is contained in:
2025-07-03 12:24:18 +02:00
committed by GitHub
23 changed files with 1062 additions and 324 deletions
+6
View File
@@ -1 +1,7 @@
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX 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
+21 -2
View File
@@ -37,8 +37,27 @@ jobs:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
- name: Set VITE_GA_MEASUREMENT_ID - name: Set environment variables
run: echo "VITE_GA_MEASUREMENT_ID=${{ secrets.VITE_GA_MEASUREMENT_ID }}" > .env 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 - name: Install dependencies
run: npm ci --omit=dev run: npm ci --omit=dev
+351
View File
@@ -0,0 +1,351 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="792" height="792" viewBox="0 0 792 792">
<style>
#body { fill: #e44c65; }
#bg { fill: #292323; }
#eye { fill: #292323; }
#opaque_bg { opacity: 1; fill: #9c9999; }
#opaque_body { opacity: 1; fill: #71706e; }
#qr { fill: #000000; }
@media (prefers-color-scheme: dark) {
#bg { fill: #ffffff; }
#eye { fill: #ffffff; }
#opaque_bg { opacity: 0.15; fill: #ffffff; }
#opaque_body { opacity: 0.3; fill: #ffffff; }
#qr { fill: #ffffff; }
}
</style>
<g id="qr">
<circle cx="3" cy="3" r="3" transform="translate(240,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,48) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(456,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,72) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,96) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,96) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,96) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,96) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,96) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(456,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,120) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,144) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,168) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,192) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,216) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(96,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,240) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,264) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(72,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(96,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,288) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(72,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,312) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(168,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,336) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(96,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,360) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(72,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,384) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(72,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,408) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,432) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(72,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,456) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(96,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(168,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(216,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,480) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(96,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(168,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(432,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,504) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(48,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(120,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(144,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(192,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,528) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,552) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,576) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(312,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,600) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(456,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(720,624) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,648) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(240,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(456,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(600,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,672) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(456,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(480,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(528,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(696,696) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(264,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(288,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(336,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(360,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(384,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(408,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(504,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(552,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(576,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(624,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(648,720) scale(4.12)"/>
<circle cx="3" cy="3" r="3" transform="translate(672,720) scale(4.12)"/>
<path d="M4.5,14h5.1C12,14,14,12,14,9.6V4.5C14,2,12,0,9.5,0H4.4C2,0,0,2,0,4.4v5.1C0,12,2,14,4.5,14z M12,4.8v4.4 c0,1.5-1.3,2.8-2.8,2.8H4.8C3.2,12,2,10.8,2,9.2V4.8C2,3.3,3.3,2,4.8,2h4.4C10.8,2,12,3.2,12,4.8z" transform="translate(48,48) scale(12)"/>
<path d="M4.5,14h5.1C12,14,14,12,14,9.6V4.5C14,2,12,0,9.5,0H4.4C2,0,0,2,0,4.4v5.1C0,12,2,14,4.5,14z M12,4.8v4.4 c0,1.5-1.3,2.8-2.8,2.8H4.8C3.2,12,2,10.8,2,9.2V4.8C2,3.3,3.3,2,4.8,2h4.4C10.8,2,12,3.2,12,4.8z" transform="translate(576,48) scale(12)"/>
<path d="M4.5,14h5.1C12,14,14,12,14,9.6V4.5C14,2,12,0,9.5,0H4.4C2,0,0,2,0,4.4v5.1C0,12,2,14,4.5,14z M12,4.8v4.4 c0,1.5-1.3,2.8-2.8,2.8H4.8C3.2,12,2,10.8,2,9.2V4.8C2,3.3,3.3,2,4.8,2h4.4C10.8,2,12,3.2,12,4.8z" transform="translate(48,576) scale(12)"/>
<rect width="6" height="6" transform="translate(96,96) scale(12)"/>
<rect width="6" height="6" transform="translate(624,96) scale(12)"/>
<rect width="6" height="6" transform="translate(96,624) scale(12)"/>
</g>
<g id="logo" display="inline" transform="matrix(14,0,0,13.5,640,802)">
<path id="body" stroke-width="0.139944" fill-rule="evenodd"
d="m -17.780346,-23.927296 c -0.740502,-0.204654 -1.19858,-0.50646 -1.708866,-0.912419 -0.195581,-0.155595 -1.180741,-0.921079 -1.587656,-0.186959 -1.282534,-1.269981 -0.904624,-3.38856 -0.223378,-4.775114 0.302528,-0.380694 0.649272,-0.729003 0.96505,-1.091677 -0.228696,-0.825519 -0.351618,-3.291542 1.987329,-3.895113 0.972192,0.0552 1.717975,1.013344 1.712946,2.177118 -0.0086,1.994014 -3.185616,2.50384 -3.338799,5.024277 -0.124698,2.051749 0.780153,3.095667 2.64463,3.740202 0,0 -0.278155,-0.03246 -0.451256,-0.0803 z" />
<path id="opaque_bg" stroke-width="0.139944"
d="m -16.525233,-25.802896 c -0.431571,-0.155018 -0.651471,-0.401725 -0.807251,-0.620025 -0.936017,1.334687 -3.339164,-1.536579 0.55272,-2.4916 -1.12997,0.424632 -0.813709,1.899893 -0.738003,1.996262 0.04362,-0.672993 0.763138,-1.09084 1.517398,-1.059535 0.21776,-0.0046 0.344574,0.03097 0.535276,0.123046 0.175909,0.101143 0.338175,0.225574 0.447391,0.376562 -0.653075,-0.542532 -1.44499,0.429431 -0.991579,0.870799 0.414816,0.434889 0.131767,0.811205 -0.0068,0.843304 -0.177973,0.0645 -0.331948,0.0028 -0.509175,-0.03881 z m 1.507219,-1.675067 c 0.24031,-0.425117 0.456162,-0.777983 1.455832,-0.921665 0.126769,-0.904263 0.619657,-1.16869 1.367029,-1.300232 -0.466802,1.263568 -1.425461,2.318603 -2.747131,2.489602 -0.140204,-1.5e-5 -0.147425,-0.148586 -0.07573,-0.267705 z m -7.812181,-1.920908 c 0.310588,0.01968 0.800422,0.21683 0.902047,0.318455 l -0.177713,0.294168 c -0.08445,-0.336394 -0.444015,-0.466598 -0.724334,-0.612623 z m 0.364831,-0.550187 c -0.119723,-0.286303 -0.161624,-0.50318 -0.612134,-0.641661 l -0.07274,-0.394547 c 1.059661,-0.06702 1.540798,0.430742 1.805881,1.19285 -0.172096,0.180593 -0.335411,0.362047 -0.488409,0.565322 -0.156151,-0.263454 -0.277954,-0.541222 -0.632608,-0.721964 z m 8.452337,-3.056927 -0.498571,0.100443 -0.174332,-0.482029 0.537781,-0.08845 0.217689,-0.247478 0.285717,-0.48926 -0.271865,-0.260991 -0.152046,0.04815 -0.116219,-0.07262 -0.114406,-0.04673 -0.518151,-0.102346 c -0.08553,0.197486 -0.165289,0.399386 -0.26161,0.588614 -0.106002,-0.1626 -0.226174,-0.300579 -0.394125,-0.433313 l 0.06694,-0.06811 0.216195,-0.523535 0.117247,-0.06813 0.943714,0.316629 0.315949,-0.04145 0.04256,0.109617 0.641359,0.494004 0.155872,-0.05923 c 0.155937,-0.04127 0.305005,-0.04907 0.454811,-0.06045 0.08197,0.123963 0.150547,0.250758 0.212858,0.378877 l -0.267391,0.01312 -0.401582,0.09235 -0.283086,0.19474 -0.286495,0.445961 0.05465,0.0746 0.112217,0.02108 0.139213,0.01356 0.05593,0.161827 0.03338,0.002 0.0054,0.113294 -0.05456,0.02166 c -0.150894,-0.04082 -0.198671,-0.03397 -0.310541,-0.01966 l -0.04879,0.07945 -0.14554,4.2e-4 z m -0.1817,-3.910579 c -18.3142,-12.855079 -18.3142,-12.855079 0,0 z m -1.386806,0.559437 -0.06544,-0.635205 -0.05468,-0.430235 -0.110769,-0.224401 v -0.05738 -0.05738 l 0.04628,0.0013 0.04628,0.0013 0.11108,0.0953 0.111081,0.0953 0.09785,0.232557 0.07423,0.171476 0.07423,0.171474 0.07035,0.119051 -0.0053,0.255837 0.05775,0.136356 0.429522,-0.104527 0.03372,-0.05754 0.113067,-0.101677 0.159348,0.139552 0.09473,0.0353 0.145295,0.173067 -0.116613,0.07124 -0.329849,0.04556 -0.530147,0.154936 -0.520678,0.528666 -0.122723,0.368287 -0.0016,0.557746 c -0.232462,-0.140161 -0.472491,-0.250597 -0.729758,-0.313503 l 0.02666,-0.03033 0.306126,-0.688346 0.0351,-0.443729 0.357901,0.05307 z" />
<path id="opaque_body" stroke-width="0.0370268"
d="m -21.300795,-29.801442 -0.05705,0.02232 c -0.140072,0.147509 -0.325447,0.352966 -0.472774,0.548961 -0.0805,0.131068 -0.183127,0.280436 -0.274805,0.444323 -0.219791,0.392905 -0.37501,0.892737 -0.409556,1.261252 0.04055,2.092768 1.064959,2.671403 1.364081,2.769177 l 0.07391,-0.271495 c -1.044292,-1.02787 -1.107279,-2.821337 -0.223803,-4.774537 z" />
<path id="bg" stroke-width="0.139944"
d="m -24.00005,-28.198155 c 0.540066,-0.206305 1.208639,0.07327 1.486003,0.673362 0.205627,1.102615 0.225643,2.227548 1.363032,2.76993 0.138024,-1.020193 1.282004,-0.476272 1.713084,-0.0611 0.534767,0.463498 1.179757,0.767742 1.866736,0.92638 1.368373,0.310783 4.967204,-0.235146 4.929394,-2.967739 0.387504,-0.303388 0.263813,-0.573515 0.3116,-0.976043 0.08838,-0.74445 0.81786,-0.856106 1.371884,-0.823218 -0.05805,7.015113 -12.334646,7.955045 -13.041733,0.458431 z m 9.241584,2.072657 c 0.145408,0.01363 0.277649,0.0075 0.410757,0.0048 -0.236519,0.426152 -0.611821,0.514387 -0.83621,0.42115 0.177779,-0.08335 0.310055,-0.257038 0.425453,-0.425887 z m -2.288667,-2.69431 c 1.4131,-0.496237 3.099837,-1.639898 2.534422,-4.085356 0.172719,-0.03853 0.338184,-0.0654 0.501048,-0.104119 0.09799,0.0769 0.203151,0.142718 0.308292,0.20909 h 0.145209 l 0.04894,-0.07918 c 0.12455,-0.02144 0.223126,-0.0087 0.310206,0.01908 l 0.05531,-0.02046 -0.0056,-0.113795 -0.03283,-0.0017 -0.056,-0.1619 -0.140933,-0.0138 -0.110407,-0.02109 -0.05485,-0.07572 c 0.08495,-0.162161 0.192391,-0.297126 0.28599,-0.446762 l 0.282196,-0.193858 0.404567,-0.08728 0.265134,-0.01578 c 0.646961,1.400723 0.665782,2.929648 0.111935,4.360422 -0.753547,0.03106 -1.338809,0.51509 -1.315785,1.258657 -0.480916,0.06166 -0.702315,0.09418 -0.866398,0.477373 -0.30325,0.05951 -0.456368,0.239528 -0.641838,0.458972 -0.822412,-0.913337 -2.375228,-0.507085 -2.502888,0.53881 -0.336543,-0.781733 -0.08326,-1.51134 0.474284,-1.901619 z m -6.088533,-2.157259 c -0.539058,-4.408364 3.611971,-7.390752 7.481325,-6.067722 l 0.07276,0.68782 -0.199,0.26325 -0.355579,-0.04717 -0.03491,0.434089 -0.302477,0.69199 -0.01597,0.01597 -0.01597,0.01597 c -2.77893,-0.220435 -4.406959,1.610741 -3.816223,4.117026 -0.348573,0.335047 -0.66657,0.695942 -0.977997,1.064826 l -0.07886,0.03053 c -0.33194,-1.049822 -0.814293,-1.165861 -1.757096,-1.206577 z m 8.056285,-3.080196 c 0.09707,-0.191174 0.179226,-0.388539 0.259856,-0.58697 l 0.519682,0.09902 0.114844,0.04595 0.116541,0.0737 0.151818,-0.04819 c 0.09857,0.06769 0.195057,0.172097 0.272691,0.261321 -0.09342,0.164788 -0.190446,0.32753 -0.286229,0.490961 l -0.216367,0.246427 c -0.181907,0.02133 -0.362188,0.05029 -0.541827,0.08539 -0.09464,-0.241763 -0.250696,-0.450895 -0.391009,-0.667616 z m 1.611498,-0.463995 -0.301659,-0.245321 -0.02144,-0.05587 -0.02144,-0.05587 -0.15764,0.02317 -0.15764,0.02317 -0.333241,-0.115178 -0.333242,-0.115179 -0.140429,-0.04334 -0.140431,-0.04334 -0.05631,0.03584 -0.05631,0.03584 -0.108156,0.256765 -0.108157,0.256765 -0.03083,0.03083 -0.03847,0.04152 c -0.09574,-0.06777 -0.198667,-0.122732 -0.301498,-0.178577 l -2.8e-4,-0.280362 -2.8e-4,-0.280362 0.0621,-0.182473 0.0621,-0.182473 0.261884,-0.265179 0.261884,-0.265178 0.263097,-0.07729 0.263097,-0.07729 0.166011,-0.02276 0.166011,-0.02275 c 0.571618,0.329796 1.362626,1.116576 1.743422,1.939135 -0.151971,-9.66e-4 -0.297574,0.02821 -0.445347,0.05846 l -0.160683,0.06398 z m -0.456181,-1.015239 c -10.824586,-2.370009 -10.824586,-2.370009 0,0 z m -1.258899,-1.083518 v -0.253131 l -0.03282,-0.05983 c 0.150432,0.06825 0.40955,0.206592 0.549691,0.291502 l -0.03426,0.05543 c -0.140835,0.05311 -0.287215,0.06142 -0.427026,0.107752 m 0.424578,0.858015 c 0,0.533333 0.8,0.533333 0.8,0 0,-0.533333 -0.8,-0.533333 -0.8,0 m 0.157809,-0.05896 c 0,0.266667 0.4,0.266667 0.4,0 0,-0.266667 -0.4,-0.266667 -0.4,0 z" />
<circle id="eye" fill="#292323" stroke-width="0.0370268" fill-opacity="1" cx="-18.741674" cy="-32.929714"
r="0.69050175" />
</g>
<image xlink:href="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDI0IDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgo8L3N2Zz4K" width="216" height="216" x="288" y="288"/>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

+9
View File
@@ -10,6 +10,7 @@ const ProjectsPage = React.lazy(() => import('components/pages/ProjectsPage/inde
const CurriculumPage = React.lazy(() => import('components/pages/CurriculumPage/index.js')) const CurriculumPage = React.lazy(() => import('components/pages/CurriculumPage/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 router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -57,6 +58,14 @@ const router = createBrowserRouter([
</React.Suspense> </React.Suspense>
), ),
}, },
{
path: 'card',
element: (
<React.Suspense fallback={<Loading />}>
<BusinessCardPage />
</React.Suspense>
),
},
], ],
}, },
]) ])
@@ -62,6 +62,9 @@ const CookieConsent = () => {
mb: theme.spacing(1), mb: theme.spacing(1),
p: theme.spacing(3), p: theme.spacing(3),
zIndex: 9999, zIndex: 9999,
'@media print': {
display: 'none',
},
})}> })}>
<Box> <Box>
<SectionSubheader id="cookie-consent-dialog-title" component="h2" variant="h6"> <SectionSubheader id="cookie-consent-dialog-title" component="h2" variant="h6">
@@ -32,10 +32,16 @@ const CurriculumItem = (props: CurriculumEntryProps) => {
// '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { color: 'primary.dark' }, // use summary hover icon color // '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { color: 'primary.dark' }, // use summary hover icon color
}, },
'&.Mui-expanded': { '&.Mui-expanded': {
bgcolor: 'background.paper', // use summary hover background
borderBottom: '1px solid', borderBottom: '1px solid',
borderTop: '1px solid', borderTop: '1px solid',
bgcolor: 'background.paper', // use summary hover background
elevation: 1, 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 // '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { color: 'primary.dark' }, // use summary hover icon color
}, },
}}> }}>
@@ -48,6 +54,9 @@ const CurriculumItem = (props: CurriculumEntryProps) => {
}, },
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(0deg)', transform: 'rotate(0deg)',
'@media print': {
transform: 'rotate(-90deg)',
},
}, },
'& .MuiAccordionSummary-content': { '& .MuiAccordionSummary-content': {
marginLeft: 2, marginLeft: 2,
+11 -1
View File
@@ -14,7 +14,17 @@ type FooterProps = {
const Footer: React.FC<FooterProps> = ({ showCookieConsent = true }) => { const Footer: React.FC<FooterProps> = ({ showCookieConsent = true }) => {
const year = new Date().getFullYear() const year = new Date().getFullYear()
return ( return (
<Box sx={{ bgcolor: 'background.paper', p: 6, display: 'flex', alignItems: 'center', flexDirection: 'column' }} component="footer"> <Box
id="footer"
component="footer"
sx={{
bgcolor: 'background.paper',
py: 6,
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
'@media print': { py: 3 },
}}>
<Stack direction="row" gap={2}> <Stack direction="row" gap={2}>
<LinkedInIconLink fontSize="large" href="https://www.linkedin.com/in/m-tartari/" /> <LinkedInIconLink fontSize="large" href="https://www.linkedin.com/in/m-tartari/" />
<GitHubIconLink fontSize="large" href="https://www.github.com/m-tartari/" /> <GitHubIconLink fontSize="large" href="https://www.github.com/m-tartari/" />
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { ParallaxBanner, ParallaxProvider } from 'react-scroll-parallax' 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 { ExpandMore } from '@mui/icons-material'
import { useViewport } from 'components/utils/viewport/index.js' import { useViewport } from 'components/utils/viewport/index.js'
@@ -125,79 +125,91 @@ const Description = (props: Props) => {
} }
} }
const DesktopComponent = React.forwardRef<HTMLSelectElement, Props>((props, ref) => { const ProjectSpotlight = React.forwardRef<HTMLSelectElement, Props>((props, ref) => {
return ( const { width } = useViewport()
<section ref={ref}>
<ParallaxProvider>
<ParallaxBanner
layers={[
{
// translateY: [0, 30],
// scale: [1, 0.5, 'easeOutCubic'],
// shouldAlwaysCompleteAnimation: true,
image: props.image,
expanded: false,
speed: -20,
},
]}
style={{
// width: '150%',
// marginLeft: '-25%',
minHeight: '100vh',
position: 'relative',
height: '100vh',
overflow: 'hidden',
}}>
<Description {...props} />
{props.scrollTarget && (
<IconButton
size="large"
onClick={() => (props.scrollTarget as React.RefObject<HTMLSelectElement | null>).current?.scrollIntoView()}
sx={{ position: 'absolute', p: 1, bottom: 2, left: '50%', transform: 'translate(-50%, 0)' }}>
<ExpandMore fontSize="large" />
</IconButton>
)}
</ParallaxBanner>
</ParallaxProvider>
</section>
)
})
const MobileComponent = React.forwardRef<HTMLSelectElement, Props>((props, ref) => {
return ( return (
<Box component="section"> <Box ref={ref}>
<Box
ref={ref}
component="img"
sx={{
width: '100%',
height: '100%',
maxHeight: '60vh',
}}
src={props.mobileImage ?? props.image}
/>
<Box <Box
component="section"
sx={theme => ({ sx={theme => ({
mt: theme.spacing(-1.05), display: width < theme.breakpoints.values.md ? 'none' : 'inline',
borderTopStyle: 'solid', '@media print': { display: 'none' },
borderTopWidth: theme.spacing(0.5),
borderTopColor: theme.palette.primary.main,
p: `${theme.spacing(7)} ${theme.spacing(5)} ${theme.spacing(5.5)} ${theme.spacing(5)}`,
})}> })}>
<Typography variant="h4">{props.title}</Typography> <ParallaxProvider>
{props.caption} <ParallaxBanner
<Typography pt={1}>{props.children}</Typography> layers={[
{
// translateY: [0, 30],
// scale: [1, 0.5, 'easeOutCubic'],
// shouldAlwaysCompleteAnimation: true,
image: props.image,
expanded: false,
speed: -20,
},
]}
style={{
// width: '150%',
// marginLeft: '-25%',
minHeight: '100vh',
position: 'relative',
height: '100vh',
overflow: 'hidden',
}}>
<Description {...props} />
{props.scrollTarget && (
<IconButton
size="large"
onClick={() => (props.scrollTarget as React.RefObject<HTMLSelectElement | null>).current?.scrollIntoView()}
sx={{ position: 'absolute', p: 1, bottom: 2, left: '50%', transform: 'translate(-50%, 0)' }}>
<ExpandMore fontSize="large" />
</IconButton>
)}
</ParallaxBanner>
</ParallaxProvider>
</Box>
<Box
component="section"
sx={theme => ({
display: width >= theme.breakpoints.values.md ? 'none' : 'inline',
'@media print': {
display: 'inline',
height: '100%',
},
})}>
<Box
component="img"
sx={{
width: '100%',
height: '100%',
maxHeight: '60vh',
'@media print': {
maxHeight: '100%',
WebkitPrintColorAdjust: 'exact !important',
printColorAdjust: 'exact !important',
},
}}
src={props.mobileImage ?? props.image}
/>
<Box
sx={theme => ({
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',
},
})}>
<Typography variant="h4">{props.title}</Typography>
{props.caption}
<Typography pt={1}>{props.children}</Typography>
</Box>
</Box> </Box>
</Box> </Box>
) )
}) })
const ProjectSpotlight = React.forwardRef<HTMLSelectElement, Props>((props, ref) => {
const { width } = useViewport()
const theme = useTheme()
return width < theme.breakpoints.values.md ? <MobileComponent {...props} ref={ref} /> : <DesktopComponent {...props} ref={ref} />
})
export default ProjectSpotlight export default ProjectSpotlight
+47 -48
View File
@@ -15,22 +15,30 @@ import {
Stack, Stack,
Toolbar, Toolbar,
Typography, Typography,
TypographyProps,
styled, styled,
useScrollTrigger, useScrollTrigger,
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import MenuIcon from '@mui/icons-material/Menu' import MenuIcon from '@mui/icons-material/Menu'
const AppBarButton = styled(Button<typeof RouterLink>)(({ theme }) => ({ const TypographyLink = styled(Typography)(({ theme }) => ({
textTransform: 'none', role: 'link',
color: theme.palette.mode === 'dark' ? 'inherit' : 'text.secondary', textDecoration: 'none',
':hover': { ...(theme.palette.mode === 'dark' && {
backgroundColor: 'transparent', color: 'inherit',
...(theme.palette.mode === 'dark' && { ':hover': {
color: theme.palette.primary.main, 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 }) => <TypographyLink component={RouterLink} variant="h6" {...props} />
const DrawerButton = styled(ListItemButton<typeof RouterLink>)(({ theme }) => ({ const DrawerButton = styled(ListItemButton<typeof RouterLink>)(({ theme }) => ({
textTransform: 'none', textTransform: 'none',
color: 'inherit', color: 'inherit',
@@ -102,6 +110,7 @@ const CustomToolbar: React.FC<CustomToolbarProps> = ({ drawerWidth = 240, window
{...props} {...props}
sx={{ sx={{
backgroundColor: trigger ? 'background.paper' : 'transparent', backgroundColor: trigger ? 'background.paper' : 'transparent',
'@media print': { backgroundColor: 'background.default' },
...(theme.palette.mode === 'light' && { ...(theme.palette.mode === 'light' && {
backgroundColor: 'hsla(0, 0%, 100%, 0.75)', backgroundColor: 'hsla(0, 0%, 100%, 0.75)',
backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
@@ -115,6 +124,7 @@ const CustomToolbar: React.FC<CustomToolbarProps> = ({ drawerWidth = 240, window
<Toolbar <Toolbar
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center',
// maxWidth: '100%', // maxWidth: '100%',
// [theme.breakpoints.up('md')]: { width: theme => theme.breakpoints.values.lg, mx: 'auto' }, // [theme.breakpoints.up('md')]: { width: theme => theme.breakpoints.values.lg, mx: 'auto' },
}}> }}>
@@ -123,58 +133,47 @@ const CustomToolbar: React.FC<CustomToolbarProps> = ({ drawerWidth = 240, window
aria-label="open drawer" aria-label="open drawer"
edge="start" edge="start"
onClick={handleDrawerToggle} 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' },
}}>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<AppBarButton
component={RouterLink} <AppBarLink
to="/" to="/"
disableRipple
sx={theme => ({ sx={theme => ({
color: theme.palette.text.primary,
[theme.breakpoints.down('md')]: { mx: 'auto' }, [theme.breakpoints.down('md')]: { mx: 'auto' },
[theme.breakpoints.up('md')]: { mr: 'auto' }, [theme.breakpoints.up('md')]: { mr: 'auto' },
})}> })}>
<Typography variant="h6">Michele Tartari</Typography> Michele Tartari
</AppBarButton> </AppBarLink>
<Stack component="div" direction="row" spacing={2} sx={{ display: { xs: 'none', md: 'flex' } }}> <Stack component="div" direction="row" spacing={2} sx={{ display: { xs: 'none', md: 'flex', alignItems: 'center' } }}>
<AppBarButton component={RouterLink} to="/" disableRipple> <AppBarLink to="/">Home</AppBarLink>
<Typography variant="h6">Home</Typography> <AppBarLink to="/projects">Projects</AppBarLink>
</AppBarButton> <AppBarLink to="/curriculum">Curriculum</AppBarLink>
<AppBarButton component={RouterLink} to="/projects" disableRipple> <Button component={RouterLink} to="/contacts" color="primary" variant="contained" sx={{ textTransform: 'none' }}>
<Typography variant="h6">Projects</Typography>
</AppBarButton>
<AppBarButton component={RouterLink} to="/curriculum" disableRipple>
<Typography variant="h6">Curriculum</Typography>
</AppBarButton>
<Button
component={RouterLink}
to="/contacts"
color="primary"
variant="contained"
sx={{
textTransform: 'none',
}}>
<Typography variant="h6">Hire me</Typography> <Typography variant="h6">Hire me</Typography>
</Button> </Button>
</Stack> </Stack>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<nav> <Drawer
<Drawer variant="temporary"
variant="temporary" open={mobileOpen}
open={mobileOpen} onClose={handleDrawerToggle}
onClose={handleDrawerToggle} ModalProps={{
ModalProps={{ keepMounted: true, // Better open performance on mobile.
keepMounted: true, // Better open performance on mobile. }}
}} sx={{
sx={{ display: { xs: 'block', md: 'none' },
display: { xs: 'block', md: 'none' }, '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, }}>
}}> {drawer}
{drawer} </Drawer>
</Drawer>
</nav>
</> </>
) )
} }
@@ -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(<BusinessCardPage />)
// 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(<BusinessCardPage />)
// 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(<BusinessCardPage />)
await user.click(screen.getByText('Print Business Card'))
expect(mockPrint).toHaveBeenCalledTimes(1)
})
it('renders correct number of business cards', () => {
render(<BusinessCardPage />)
const businessCards = screen.getAllByRole('img', { name: /logo|qr code/i })
expect(businessCards).toHaveLength(2)
})
})
@@ -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]'
@@ -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;
}
}
@@ -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<typeof Link, { icon: typeof SvgIcon }>) => (
<Box
component={Link}
rel="noopener noreferrer"
target="_blank"
underline="hover"
color="text.secondary"
{...props}
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ...props.sx }}>
<Icon
sx={{
color: 'text.primary',
fontSize: '0.7rem',
}}
/>
<Typography variant="caption" color="text.secondary" fontSize="0.55rem">
{children}
</Typography>
</Box>
)
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) => (
<ThemeProvider theme={theme[mode]}>
<Paper
className="print-business-card"
elevation={1}
sx={{
colorScheme: `${mode} !important`,
backgroundColor: 'background.default',
width: '85mm',
height: '55mm',
padding: '6mm',
borderRadius: 2,
borderWidth: 1,
borderStyle: 'solid',
borderColor: mode === 'light' ? 'background.default' : 'white',
display: 'flex',
alignItems: 'center',
gap: 1.5,
'@media print': {
backgroundColor: mode === 'light' ? 'white' : 'background.default',
WebkitPrintColorAdjust: 'exact !important',
printColorAdjust: 'exact !important',
elevation: 0,
borderRadius: 0,
borderColor: 'transparent',
gap: 1.5,
},
...sx,
}}
{...props}>
<Box
component="img"
src={src}
alt={alt}
sx={{
width: '30%',
height: 'fit-content',
objectFit: 'cover',
// borderRadius: '50%',
}}
/>
{/* Right side with content */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}>
{props.children}
</Box>
</Paper>
</ThemeProvider>
)
/**
* BusinessCardPage component - A digital business card
* Designed to be printable as a standard business card (3.5 x 2 inches)
*/
const BusinessCardPage = () => {
return (
<Page>
<Stack
sx={{
justifyContent: 'center',
alignItems: 'center',
pt: 4,
gap: 2,
'@media print': {
pt: 0,
gap: 0,
},
}}>
<BusinessCard src="/fav-icon.svg" alt="logo">
<Typography variant="h5" component="h1" fontFamily="Oswald, sans-serif" textTransform="uppercase" fontWeight={700}>
{env.NAME}
</Typography>
<Typography variant="subtitle2" color="text.secondary" sx={{ fontSize: '0.55rem', mb: 0.5 }}>
{env.POSITION}
</Typography>
</BusinessCard>
<BusinessCard src="/images/qr.svg" alt="Qr Code" mode="light">
<Stack
sx={{
mt: 0.5,
borderLeft: '1px solid',
borderColor: 'divider',
pl: 2,
}}>
<Typography
variant="subtitle2"
color="text.secondary"
fontSize="0.55rem"
fontFamily="Oswald, sans-serif"
textTransform="uppercase"
fontWeight={700}>
{env.NAME}
</Typography>
<Typography variant="caption" color="text.secondary" fontSize="0.55rem" mb={0.5}>
{env.VAT}
</Typography>
<TypographyLink icon={PhoneInTalk} href={`tel:${env.PHONE.replace(/[^\d+]/g, '').replace(/^\+?/, '+')}`}>
{env.PHONE}
</TypographyLink>
<TypographyLink icon={Email} href={`mailto:${env.EMAIL}`}>
{env.EMAIL}
</TypographyLink>
<TypographyLink icon={Public} href="https://www.m-tartari.eu">
www.m-tartari.eu
</TypographyLink>
<TypographyLink icon={LinkedIn} href={`https://www.linkedin.com/in/${env.LINKEDIN}/`}>
{env.LINKEDIN}
</TypographyLink>
</Stack>
</BusinessCard>
</Stack>
<Box
sx={{
ariaLabel: 'Print Business Card',
my: 7,
display: 'flex',
justifyContent: 'center',
'@media print': {
display: 'none',
},
}}>
<Button variant="contained" startIcon={<Print />} onClick={() => window.print()}>
Print Business Card
</Button>
</Box>
</Page>
)
}
export default BusinessCardPage
@@ -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 { Award, ExternalIconLink, Extracurricular, Publication, SectionHeader, SectionSubheader } from 'components/atoms/index.js'
import { CurriculumItem } from 'components/molecules/index.js' import { CurriculumItem } from 'components/molecules/index.js'
@@ -12,20 +12,18 @@ const Curriculum = () => {
{/* id="selfemployed-software" */} {/* id="selfemployed-software" */}
<CurriculumItem title="Software Engineering Consultant" dates="04/24 - Today" company="Self-Employed" location="Remote"> <CurriculumItem title="Software Engineering Consultant" dates="04/24 - Today" company="Self-Employed" location="Remote">
<Typography> <Typography component="ul" sx={{ paddingLeft: '1em', marginTop: 0 }}>
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <li>
<li> Design and development of customized graphical interfaces and software components to facilitate the use of robots and industrial
Design and development of customized graphical interfaces and software components to facilitate the use of robots and industrial machines.
machines. </li>
</li> <li>Production and maintenance of revisions of the documentation.</li>
<li>Production and maintenance of revisions of the documentation.</li> <li>Providing technical assistance for R&D activities.</li>
<li>Providing technical assistance for R&D activities.</li>
</ul>
</Typography> </Typography>
</CurriculumItem> </CurriculumItem>
{/* id="MinHub-lead" */} {/* id="MinHub-lead" */}
<CurriculumItem title="Business Partner & Technical Lead" dates="03/23 - 12/24" company="MindHub Ferrara" location="Ferrara, Italy"> <CurriculumItem title="Business Partner & Technical Lead" dates="03/23 - 12/24" company="MindHub Ferrara" location="Ferrara, Italy">
<Typography> <Typography component="div">
Co-founded{' '} Co-founded{' '}
<Link <Link
target="_blank" target="_blank"
@@ -49,7 +47,7 @@ const Curriculum = () => {
</CurriculumItem> </CurriculumItem>
{/* id="IIT-software" */} {/* id="IIT-software" */}
<CurriculumItem title="Software Engineer" dates="06/21 - 04/24" company="Istituto Italiano di Tecnologia" location="Remote"> <CurriculumItem title="Software Engineer" dates="06/21 - 04/24" company="Istituto Italiano di Tecnologia" location="Remote">
<Typography> <Typography component="div">
Worked in{' '} Worked in{' '}
<Link target="_blank" rel="noopener noreferrer" href="https://web.archive.org/web/20220706131402/https://alberobotics.it/"> <Link target="_blank" rel="noopener noreferrer" href="https://web.archive.org/web/20220706131402/https://alberobotics.it/">
Alberobotics Alberobotics
@@ -76,7 +74,7 @@ const Curriculum = () => {
</CurriculumItem> </CurriculumItem>
{/* id="IIT-embedded" */} {/* id="IIT-embedded" */}
<CurriculumItem title="Embedded Engineer" dates="04/20 - 06/21" company="Istituto Italiano di Tecnologia" location="Genoa, Italy"> <CurriculumItem title="Embedded Engineer" dates="04/20 - 06/21" company="Istituto Italiano di Tecnologia" location="Genoa, Italy">
<Typography> <Typography component="div">
Worked in{' '} Worked in{' '}
<Link target="_blank" rel="noopener noreferrer" href="https://web.archive.org/web/20220706131402/https://alberobotics.it/"> <Link target="_blank" rel="noopener noreferrer" href="https://web.archive.org/web/20220706131402/https://alberobotics.it/">
Alberobotics Alberobotics
@@ -104,7 +102,7 @@ const Curriculum = () => {
dates="01/19 - 06/19" dates="01/19 - 06/19"
company="Warsaw University of Technology - Division of Theory of Machines and Robots" company="Warsaw University of Technology - Division of Theory of Machines and Robots"
location="Warsaw, Poland"> location="Warsaw, Poland">
<Typography> <Typography component="div">
My main duties included: My main duties included:
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <ul style={{ paddingLeft: '1em', marginTop: 0 }}>
<li>Developing a voice interface and some user detection strategies for low-power robotic systems using ROS.</li> <li>Developing a voice interface and some user detection strategies for low-power robotic systems using ROS.</li>
@@ -115,7 +113,7 @@ const Curriculum = () => {
</CurriculumItem> </CurriculumItem>
{/* id="WUT-intern" */} {/* id="WUT-intern" */}
<CurriculumItem title="Researcher - Internship" dates="01/17 - 02/17" company="LAR - University of Bologna" location="Bologna, Italy"> <CurriculumItem title="Researcher - Internship" dates="01/17 - 02/17" company="LAR - University of Bologna" location="Bologna, Italy">
<Typography> <Typography component="div">
My main duties included: My main duties included:
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <ul style={{ paddingLeft: '1em', marginTop: 0 }}>
<li>Developing a setup to control a shape memory alloy material using an embedded PC (a Beagle Bone Black).</li> <li>Developing a setup to control a shape memory alloy material using an embedded PC (a Beagle Bone Black).</li>
@@ -157,26 +155,24 @@ const Curriculum = () => {
// (grade: 5/5,summa cum laude) // (grade: 5/5,summa cum laude)
location="Warsaw, Poland" location="Warsaw, Poland"
dates="09/18 - 06/19"> dates="09/18 - 06/19">
<Typography> <Typography component="ul" sx={{ paddingLeft: '1em', marginTop: 0 }}>
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <li>Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.</li>
<li>Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.</li> <li>Relevant courses: Research Methodology, Bio-Robotics, Dynamics of Multi-Body Systems, Advanced Mechanical Design.</li>
<li>Relevant courses: Research Methodology, Bio-Robotics, Dynamics of Multi-Body Systems, Advanced Mechanical Design.</li> <li>Other relevant courses: 2 exams of Polish language and 1 of Polish culture.</li>
<li>Other relevant courses: 2 exams of Polish language and 1 of Polish culture.</li> <li>
<li> Thesis:{' '}
Thesis:{' '} <Link
<Link target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" href="https://drive.google.com/drive/folders/1Y7TJJN8z1llCkSTnpVHMELD3PqsX8VF9?usp=sharing">
href="https://drive.google.com/drive/folders/1Y7TJJN8z1llCkSTnpVHMELD3PqsX8VF9?usp=sharing"> Voice Controlled Mobile Robot for Indoor Applications
Voice Controlled Mobile Robot for Indoor Applications </Link>{' '}
</Link>{' '} (research publication available{' '}
(research publication available{' '} <Link target="_blank" rel="noopener noreferrer" href="https://doi.org/10.1007/978-3-030-58380-4_7">
<Link target="_blank" rel="noopener noreferrer" href="https://doi.org/10.1007/978-3-030-58380-4_7"> <em>here</em>
<em>here</em> </Link>
</Link> ).
). </li>
</li>
</ul>
</Typography> </Typography>
</CurriculumItem> </CurriculumItem>
{/* id="ECN" */} {/* id="ECN" */}
@@ -185,25 +181,23 @@ const Curriculum = () => {
company="Ecole Centrale de Nantes" company="Ecole Centrale de Nantes"
location="Nantes, France" location="Nantes, France"
dates="09/17 - 06/18"> dates="09/17 - 06/18">
<Typography> <Typography component="ul" sx={{ paddingLeft: '1em', marginTop: 0 }}>
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <li>Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.</li>
<li>Consortium scholarship issued for EMARO+ students based on academic merits and offered to top applicants only.</li> <li>
<li> Relevant courses: Mobile Robotics, Non-linear Control Theory, Software Architecture for Robotics, Artificial Intelligence, Computer
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.
Vision, Real Time Systems, Mechanical Design Methods in Robotics, Optimization Techniques. </li>
</li> <li>Other relevant courses: 2 exams of French language.</li>
<li>Other relevant courses: 2 exams of French language.</li> <li>
<li> Project:{' '}
Project:{' '} <Link
<Link target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" href="https://drive.google.com/file/d/1wBfvAzy9N8uChNxNj9Kd_hcvI0Qq3uQj/view?usp=sharing">
href="https://drive.google.com/file/d/1wBfvAzy9N8uChNxNj9Kd_hcvI0Qq3uQj/view?usp=sharing"> Recharging electric cars with a mobile robot
Recharging electric cars with a mobile robot </Link>
</Link> .
. </li>
</li>
</ul>
</Typography> </Typography>
</CurriculumItem> </CurriculumItem>
@@ -227,18 +221,16 @@ const Curriculum = () => {
// (grade: 5/5) // (grade: 5/5)
location="Shanghai, China" location="Shanghai, China"
dates="09/14 - 06/17"> dates="09/14 - 06/17">
<Typography> <Typography component="ul" sx={{ paddingLeft: '1em', marginTop: 0 }}>
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <li>Relevant courses: Automation Controls, Electrical Circuit, Logical Design of Digital Systems.</li>
<li>Relevant courses: Automation Controls, Electrical Circuit, Logical Design of Digital Systems.</li> <li>Other relevant courses: 2 Exams about Chinese Language and 3 Exams about Chinese Culture.</li>
<li>Other relevant courses: 2 Exams about Chinese Language and 3 Exams about Chinese Culture.</li> <li>
<li> Thesis:{' '}
Thesis:{' '} <Link target="_blank" rel="noopener noreferrer" href="https://drive.google.com/file/d/0B1z42QMR-0UtYzBYSVhRV3ZPWk0/view?usp=sharing">
<Link target="_blank" rel="noopener noreferrer" href="https://drive.google.com/file/d/0B1z42QMR-0UtYzBYSVhRV3ZPWk0/view?usp=sharing"> Solar Panel Fault Detection Method Based on Infrared Image Processing
Solar Panel Fault Detection Method Based on Infrared Image Processing </Link>
</Link> .
. </li>
</li>
</ul>
</Typography> </Typography>
</CurriculumItem> </CurriculumItem>
{/* id="UniBo" */} {/* id="UniBo" */}
@@ -248,19 +240,17 @@ const Curriculum = () => {
location="Bologna, Italy" location="Bologna, Italy"
//(grade: 102/110) //(grade: 102/110)
dates="09/13 - 12/16"> dates="09/13 - 12/16">
<Typography> <Typography component="ul" sx={{ paddingLeft: '1em', marginTop: 0 }}>
<ul style={{ paddingLeft: '1em', marginTop: 0 }}> <li>Scholarship issued for Almatong Project student based on academic merits and offered to top applicants only.</li>
<li>Scholarship issued for Almatong Project student based on academic merits and offered to top applicants only.</li> <li>Relevant courses: Foundation of Industrial Robotics, Automatic Machines, Computer Architectures.</li>
<li>Relevant courses: Foundation of Industrial Robotics, Automatic Machines, Computer Architectures.</li> <li>Other relevant courses: 2 exams of French language.</li>
<li>Other relevant courses: 2 exams of French language.</li> <li>
<li> Thesis:{' '}
Thesis:{' '} <Link target="_blank" rel="noopener noreferrer" href="https://drive.google.com/file/d/0B1z42QMR-0UtSkxRVzJPTk1IX0k/view?usp=sharing">
<Link target="_blank" rel="noopener noreferrer" href="https://drive.google.com/file/d/0B1z42QMR-0UtSkxRVzJPTk1IX0k/view?usp=sharing"> Design of a Testing Machine for Flexure Hinges
Design of a Testing Machine for Flexure Hinges </Link>
</Link> .
. </li>
</li>
</ul>
</Typography> </Typography>
</CurriculumItem> </CurriculumItem>
@@ -305,50 +295,52 @@ const Curriculum = () => {
<Divider sx={{ mt: 4, mb: 4 }} /> <Divider sx={{ mt: 4, mb: 4 }} />
<SectionHeader>Skills</SectionHeader> <SectionHeader>Skills</SectionHeader>
<Table> <Table>
<TableRow> <TableBody>
<TableCell width="15%"> <TableRow>
<Typography variant="h6" component="b" fontWeight={600}> <TableCell width="15%">
Languages <Typography variant="h6" component="b" fontWeight={600}>
</Typography> Languages
</TableCell> </Typography>
<TableCell width="85%">Italian (Native Speaker), English (Proficient, IELTS 7.5), French (Good), Chinese (Basic, HSK3)</TableCell> </TableCell>
</TableRow> <TableCell width="85%">Italian (Native Speaker), English (Proficient, IELTS 7.5), French (Good), Chinese (Basic, HSK3)</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell rowSpan={4} width="15%" id="digital_skills"> <TableCell rowSpan={4} width="15%" id="digital_skills">
<Typography variant="h6" component="b" fontWeight={600}> <Typography variant="h6" component="b" fontWeight={600}>
Digital Digital
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell width="85%"> <TableCell width="85%">
<b>Languages:</b> TypeScript/Javascript (Proficient), Python (Proficient), Bash (Good), C++ (Good), C (Fair) <b>Languages:</b> TypeScript/Javascript (Proficient), Python (Proficient), Bash (Good), C++ (Good), C (Fair)
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<b>Softwares:</b> git (Proficient), Matlab (Good), LaTeX (Good), CATIA (Fair) <b>Softwares:</b> git (Proficient), Matlab (Good), LaTeX (Good), CATIA (Fair)
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<b>Middleware:</b> Robotic Operative System (ROS, Good) <b>Middleware:</b> Robotic Operative System (ROS, Good)
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<b>OS Used:</b> Linux, Microsoft <b>OS Used:</b> Linux, Microsoft
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow sx={{ '&:last-child td': { borderBottom: 0 } }}> <TableRow sx={{ '&:last-child td': { borderBottom: 0 } }}>
<TableCell width="15%"> <TableCell width="15%">
<Typography variant="h6" component="b" fontWeight={600}> <Typography variant="h6" component="b" fontWeight={600}>
Softskills Softskills
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell width="85%"> <TableCell width="85%">
Adaptability, Communication, Creativity, Critical Thinking, Problem-Solving, Self-Organization, Teamworking Adaptability, Communication, Creativity, Critical Thinking, Problem-Solving, Self-Organization, Teamworking
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody>
</Table> </Table>
{/* Accomplishments */} {/* Accomplishments */}
@@ -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' import { RouterProvider, createMemoryRouter } from 'react-router-dom'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useAnalytics } from 'use-analytics' import { useAnalytics } from 'use-analytics'
@@ -10,19 +10,14 @@ vi.mock('use-analytics', () => ({
useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }), useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }),
})) }))
// Tests // Suppress console.error messages during tests
describe('Renders ErrorPage correctly', async () => { const originalConsoleError = console.error
/** console.error = vi.fn()
* Resets all renders after each test
*/
afterEach(() => {
cleanup()
})
/** describe('Renders ErrorPage correctly', async () => {
* Passes - shows title correctly
*/
it('Should render the error page correctly', () => { it('Should render the error page correctly', () => {
console.error = vi.fn()
const badRoute = '/some/bad/route' const badRoute = '/some/bad/route'
const router = createMemoryRouter([{ path: '/', element: <div />, errorElement: <ErrorPage /> }], { initialEntries: [badRoute] }) const router = createMemoryRouter([{ path: '/', element: <div />, errorElement: <ErrorPage /> }], { initialEntries: [badRoute] })
@@ -33,3 +28,5 @@ describe('Renders ErrorPage correctly', async () => {
expect(screen.getByText(`404 Error: No route matches URL "${badRoute}"`)).toBeInTheDocument() expect(screen.getByText(`404 Error: No route matches URL "${badRoute}"`)).toBeInTheDocument()
}) })
}) })
console.error = originalConsoleError
@@ -18,25 +18,10 @@ describe('Main page', () => {
// Make sure the page title is correct // Make sure the page title is correct
expect(screen.getByText('Robotics Engineer')).toBeInTheDocument() expect(screen.getByText('Robotics Engineer')).toBeInTheDocument()
// Make sure the project spotlights are present // Make sure the project spotlights are present for both desktop and mobile
expect(screen.getByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toBeInTheDocument() expect(screen.getAllByText('LINFA: An Intuitive Block-Programming Interface for Reconfigurable Robots')).toHaveLength(2)
expect(screen.getByText("Revamping HHCM's Modular: Swift Generation of Modular Robot Models")).toBeInTheDocument() expect(screen.getAllByText("Revamping HHCM's Modular: Swift Generation of Modular Robot Models")).toHaveLength(2)
expect(screen.getByText('Automated Actuators and Robotic Modules Calibration')).toBeInTheDocument() expect(screen.getAllByText('Automated Actuators and Robotic Modules Calibration')).toHaveLength(2)
expect(screen.getByText('Voice Controlled Mobile Robot for Indoor Applications')).toBeInTheDocument() expect(screen.getAllByText('Voice Controlled Mobile Robot for Indoor Applications')).toHaveLength(2)
})
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(<MainPage />)
// 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()
}) })
}) })
+1
View File
@@ -2,5 +2,6 @@ export { default as MainPage } from './MainPage'
export { default as ContactPage } from './ContactPage' export { default as ContactPage } from './ContactPage'
export { default as CurriculumPage } from './CurriculumPage' export { default as CurriculumPage } from './CurriculumPage'
export { default as ProjectsPage } from './ProjectsPage' export { default as ProjectsPage } from './ProjectsPage'
export { default as BusinessCardPage } from './BusinessCardPage'
export { default as ErrorPage } from './ErrorPage' export { default as ErrorPage } from './ErrorPage'
+8 -2
View File
@@ -19,11 +19,17 @@ const Page: React.FC<PageProps> = ({ title, children, showCookieConsent, ...prop
}, [title]) }, [title])
return ( return (
<Box sx={{ flexGrow: 1 }} aria-label="page"> <Box
aria-label="page"
sx={{
'@media print': { backgroundColor: 'background.default' },
flexGrow: 1,
}}>
<Toolbar /> <Toolbar />
{title && ( {title && (
<> <>
<Typography <Typography
id="page-title"
variant="h1" variant="h1"
aria-label="page-title" aria-label="page-title"
textAlign="center" textAlign="center"
@@ -53,8 +59,8 @@ const Page: React.FC<PageProps> = ({ title, children, showCookieConsent, ...prop
</> </>
)} )}
<Container <Container
id="page-content"
component="main" component="main"
maxWidth={false}
sx={{ sx={{
mt: { xs: 8, md: 12 }, mt: { xs: 8, md: 12 },
mb: 4, mb: 4,
+5
View File
@@ -14,6 +14,11 @@ afterEach(() => {
cleanup() cleanup()
}) })
/**
* Mocks the window.scrollTo method (called on page change)
*/
vi.stubGlobal('scrollTo', vi.fn())
/** /**
* Mocks the ResizeObserver API (used by ParallaxProvider) * Mocks the ResizeObserver API (used by ParallaxProvider)
*/ */
+31
View File
@@ -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 }
+16 -50
View File
@@ -1,58 +1,24 @@
/* istanbul ignore file */ /* istanbul ignore file */
import { createTheme, responsiveFontSizes } from '@mui/material/styles' 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: { import typography from './typography/index.js'
fontFamily: 'Oswald, sans-serif', import palette from './palette/index.js'
textTransform: 'uppercase',
fontWeight: 700, let theme = createTheme({
}, typography: typography,
h2: {
fontFamily: 'Oswald, sans-serif',
textTransform: 'uppercase',
fontWeight: 700,
},
h3: {
fontFamily: 'Oswald, sans-serif',
fontWeight: 700,
},
h4: {
fontFamily: 'Oswald, sans-serif',
fontWeight: 700,
},
},
colorSchemes: { colorSchemes: {
dark: { light: { palette: palette.light },
palette: { dark: { palette: palette.dark },
primary: { },
main: '#e44c65', // Rouge Red (similar) components: {
// main:'#df4661', // Pantone PMS 198 C 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
}, },
}, },
}, },
+26
View File
@@ -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
+6
View File
@@ -2,6 +2,12 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_GA_MEASUREMENT_ID: string 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 { interface ImportMeta {