From 54237096c17cb674144c97bf90c0d3cf3e517a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elliot=20N=C3=A9grel-Jerzy?= Date: Fri, 31 May 2024 17:37:42 +0200 Subject: [PATCH 01/11] feat: create basic download banner on resume page --- src/index.tsx | 2 +- src/resume/Download.tsx | 46 +++++++++++++++++++++++++++++++++++++ src/{ => resume}/Resume.tsx | 2 ++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/resume/Download.tsx rename src/{ => resume}/Resume.tsx (99%) diff --git a/src/index.tsx b/src/index.tsx index 91cf537..29ecaeb 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ import { } from 'react-router-dom'; import { SpeedInsights } from '@vercel/speed-insights/react'; import Landing from 'Landing'; -import Resume from 'Resume'; +import Resume from 'resume/Resume'; import ReactGA from 'react-ga4'; import 'App.global.scss'; import Projects from 'components/projects/Projects'; diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx new file mode 100644 index 0000000..b0405a8 --- /dev/null +++ b/src/resume/Download.tsx @@ -0,0 +1,46 @@ +import { + Button, Card, Stack, Typography, +} from '@mui/joy'; +import details from 'assets/Details'; +import React, { useState } from 'react'; + +export default function Download() { + const [fileName] = useState(`Resume_${details.name.first}_${details.name.last}.pdf`); + return ( + + ({ + width: 'min(100%, 30em)', + backgroundColor: `color-mix(in srgb, ${theme.palette.background.body}, transparent 50%)`, + backdropFilter: 'blur(10px)', + webkitBackdropFilter: 'blur(10px)', + padding: '.5rem', + marginY: '.5rem', + })} + > + + + {fileName} + + + + + + ); +} diff --git a/src/Resume.tsx b/src/resume/Resume.tsx similarity index 99% rename from src/Resume.tsx rename to src/resume/Resume.tsx index 307727e..8f2d976 100644 --- a/src/Resume.tsx +++ b/src/resume/Resume.tsx @@ -13,6 +13,7 @@ import { Education, Experience } from 'components/Details'; import Meta from 'components/Meta'; import { useMobileMode } from 'components/Responsive'; import { marked } from 'marked'; +import Download from './Download'; export function Languages() { const color = (level: string): ColorPaletteProp => { @@ -71,6 +72,7 @@ export default function Resume() { return ( <> + Date: Fri, 31 May 2024 21:05:06 +0200 Subject: [PATCH 02/11] refactor: improve download banner responsiveness on resume page --- src/resume/Download.tsx | 108 ++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 15 deletions(-) diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx index b0405a8..4bdd140 100644 --- a/src/resume/Download.tsx +++ b/src/resume/Download.tsx @@ -1,14 +1,30 @@ import { - Button, Card, Stack, Typography, + Button, Card, IconButton, Stack, Tooltip, Typography, } from '@mui/joy'; import details from 'assets/Details'; -import React, { useState } from 'react'; +import { useMobileMode } from 'components/Responsive'; +import React, { createRef } from 'react'; +import { FaRegFilePdf } from 'react-icons/fa'; +import { FiDownload, FiPrinter } from 'react-icons/fi'; export default function Download() { - const [fileName] = useState(`Resume_${details.name.first}_${details.name.last}.pdf`); + const mobile = useMobileMode(); + + const container = createRef(); + + const fileName = `Resume_${details.name.first}_${details.name.last}.pdf`; + + const print = () => { + // noop + }; + + const download = () => { + // noop + }; + return ( ({ - width: 'min(100%, 30em)', - backgroundColor: `color-mix(in srgb, ${theme.palette.background.body}, transparent 50%)`, + borderRadius: mobile ? 0 : undefined, + width: mobile ? '100%' : '30rem', + marginBottom: mobile ? 0 : '1rem', + backgroundColor: `color-mix(in srgb, ${theme.palette.background.body}, transparent 30%)`, backdropFilter: 'blur(10px)', webkitBackdropFilter: 'blur(10px)', padding: '.5rem', - marginY: '.5rem', + boxShadow: 'lg', })} > - - - {fileName} + + + + )} + sx={{ + gap: 0.5, + flex: '1 1 100%', + minWidth: 0, + }} + > + + {fileName} + - + + {mobile ? ( + + + + + + ) : ( + + )} + {mobile ? ( + + + + + + ) : ( + + )} + From f138ea929ecd0006f7c8fe24e48655fa9c5405c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elliot=20N=C3=A9grel-Jerzy?= Date: Sat, 1 Jun 2024 14:52:30 +0200 Subject: [PATCH 03/11] feat: implement basic jsPDF printing system --- package.json | 1 + src/index.tsx | 34 ++++++--- src/navigation/NavigationBar.tsx | 4 +- src/resume/Download.tsx | 16 +++-- src/resume/Resume.tsx | 3 +- yarn.lock | 119 ++++++++++++++++++++++++++++++- 6 files changed, 156 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 2cb78dc..5048109 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "axios": "^1.6.5", "color": "^4.2.3", "i18next": "^23.11.5", + "jspdf": "^2.5.1", "marked": "^12.0.2", "moment": "^2.29.4", "react": "^18.3.1", diff --git a/src/index.tsx b/src/index.tsx index 29ecaeb..17b7028 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,7 @@ import NavigationBar from 'navigation/NavigationBar'; import AnalyticsBanner from 'components/AnalyticsBanner'; import Copyright from 'components/Copyright'; import NotFound from 'NotFound'; +import Download from 'resume/Download'; if (process.env.REACT_APP_GA_MEASUREMENT_ID) { ReactGA.initialize(process.env.REACT_APP_GA_MEASUREMENT_ID as string); @@ -32,20 +33,35 @@ root.render( - - + <> + + - - )} + + )} > - } /> - } /> - } /> + + + + )} + > + } /> + + + + + )} + /> + } /> + + } /> - } /> diff --git a/src/navigation/NavigationBar.tsx b/src/navigation/NavigationBar.tsx index df3ff40..65e620d 100644 --- a/src/navigation/NavigationBar.tsx +++ b/src/navigation/NavigationBar.tsx @@ -251,7 +251,7 @@ export default function NavigationBar({ text="Projects" layout={horizontal ? 'horizontal' : 'vertical'} to="/projects" - selected={location.pathname === '/projects'} + selected={location.pathname.startsWith('/projects')} /> } @@ -259,7 +259,7 @@ export default function NavigationBar({ text="Resume" layout={horizontal ? 'horizontal' : 'vertical'} to="/resume" - selected={location.pathname === '/resume'} + selected={location.pathname.startsWith('/resume')} /> {horizontal ? ( diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx index 4bdd140..a9de3b4 100644 --- a/src/resume/Download.tsx +++ b/src/resume/Download.tsx @@ -3,6 +3,7 @@ import { } from '@mui/joy'; import details from 'assets/Details'; import { useMobileMode } from 'components/Responsive'; +import jsPDF from 'jspdf'; import React, { createRef } from 'react'; import { FaRegFilePdf } from 'react-icons/fa'; import { FiDownload, FiPrinter } from 'react-icons/fi'; @@ -15,11 +16,15 @@ export default function Download() { const fileName = `Resume_${details.name.first}_${details.name.last}.pdf`; const print = () => { - // noop + window.print(); }; const download = () => { - // noop + // eslint-disable-next-line new-cap + const doc = new jsPDF(); + const source = window.document.getElementById('resume') as HTMLElement; + doc.html(source); + doc.save(fileName); }; return ( @@ -37,11 +42,10 @@ export default function Download() { }} > ({ - borderRadius: mobile ? 0 : undefined, - width: mobile ? '100%' : '30rem', - marginBottom: mobile ? 0 : '1rem', + width: mobile ? 'calc(100% - 1rem)' : '30rem', + margin: mobile ? '.5rem' : '0 0 1rem 0', backgroundColor: `color-mix(in srgb, ${theme.palette.background.body}, transparent 30%)`, backdropFilter: 'blur(10px)', webkitBackdropFilter: 'blur(10px)', diff --git a/src/resume/Resume.tsx b/src/resume/Resume.tsx index 8f2d976..20e396b 100644 --- a/src/resume/Resume.tsx +++ b/src/resume/Resume.tsx @@ -13,7 +13,6 @@ import { Education, Experience } from 'components/Details'; import Meta from 'components/Meta'; import { useMobileMode } from 'components/Responsive'; import { marked } from 'marked'; -import Download from './Download'; export function Languages() { const color = (level: string): ColorPaletteProp => { @@ -72,8 +71,8 @@ export default function Resume() { return ( <> - Date: Sat, 1 Jun 2024 15:38:41 +0200 Subject: [PATCH 04/11] feat: implement print function --- package.json | 1 - src/components/AnalyticsBanner.tsx | 4 ++++ src/components/Copyright.tsx | 5 +++- src/navigation/NavigationBar.tsx | 10 +++++--- src/navigation/useOverlayQueryParam.tsx | 15 ++++++++++++ src/resume/Download.tsx | 20 +++++++++++++++- yarn.lock | 32 ------------------------- 7 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 src/navigation/useOverlayQueryParam.tsx diff --git a/package.json b/package.json index 5048109..1b77eca 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/joy": "5.0.0-alpha.85", - "@react-hook/size": "^2.1.2", "@react-spring/web": "^9.7.3", "@types/color": "^3.0.3", "@types/marked": "^6.0.0", diff --git a/src/components/AnalyticsBanner.tsx b/src/components/AnalyticsBanner.tsx index d5a700b..540a7e6 100644 --- a/src/components/AnalyticsBanner.tsx +++ b/src/components/AnalyticsBanner.tsx @@ -2,11 +2,14 @@ import { Button, Card, Stack, Typography, } from '@mui/joy'; import { animated, useSpringRef, useTransition } from '@react-spring/web'; +import useOverlayQueryParam from 'navigation/useOverlayQueryParam'; import React, { useEffect, useState } from 'react'; export default function AnalyticsBanner() { const [isDimissed, setIsDimissed] = useState(localStorage.getItem('analyticsBannerDismissed') === 'true'); + const hidden = useOverlayQueryParam(); + const transitionRef = useSpringRef(); const transition = useTransition(isDimissed, { @@ -35,6 +38,7 @@ export default function AnalyticsBanner() { component={animated.div} variant="outlined" sx={(theme) => ({ + display: hidden ? 'none' : 'flex', position: 'fixed', bottom: 'var(--nav-safe-area-inset-bottom, 0)', marginBottom: '1rem', diff --git a/src/components/Copyright.tsx b/src/components/Copyright.tsx index 69370fc..55cd0b6 100644 --- a/src/components/Copyright.tsx +++ b/src/components/Copyright.tsx @@ -1,6 +1,7 @@ import { Card, Link, Typography } from '@mui/joy'; import React from 'react'; import { animated } from '@react-spring/web'; +import useOverlayQueryParam from 'navigation/useOverlayQueryParam'; import { useMobileMode } from './Responsive'; /** @@ -18,6 +19,8 @@ import { useMobileMode } from './Responsive'; export default function Copyright() { const mobile = useMobileMode(); + const hidden = useOverlayQueryParam(); + const isAuthorDomain = ['bsodium.fr', 'www.bsodium.fr'].includes(window.location.hostname); return isAuthorDomain ? null : ( @@ -29,7 +32,7 @@ export default function Copyright() { right: '0', width: mobile ? '100vw' : undefined, zIndex: 1000, - display: 'flex', + display: hidden ? 'none' : 'flex', flexDirection: 'row', borderRadius: 0, borderBottomLeftRadius: mobile ? undefined : '1rem', diff --git a/src/navigation/NavigationBar.tsx b/src/navigation/NavigationBar.tsx index 65e620d..54f5949 100644 --- a/src/navigation/NavigationBar.tsx +++ b/src/navigation/NavigationBar.tsx @@ -21,6 +21,7 @@ import { BsSun, } from 'react-icons/bs'; import { MdOutlineAutoMode } from 'react-icons/md'; +import useOverlayQueryParam from './useOverlayQueryParam'; const modes = ['light', 'dark', 'system'] as const; @@ -140,6 +141,8 @@ export default function NavigationBar({ const location = useLocation(); const { mode, setMode } = useColorScheme(); + const hidden = useOverlayQueryParam(); + const bottom = useMobileMode(); const landscape = useLandScapeMode(); const horizontal = useMemo(() => !landscape && !bottom, [landscape, bottom]); @@ -174,15 +177,15 @@ export default function NavigationBar({ useEffect(() => { document.documentElement.style.setProperty( '--nav-safe-area-inset-top', - (landscape || bottom) ? '0' : (height ? `${height}px` : '3rem'), + (landscape || bottom || hidden) ? '0' : (height ? `${height}px` : '3rem'), ); document.documentElement.style.setProperty( '--nav-safe-area-inset-bottom', - bottom ? (height ? `${height}px` : '4.5rem') : '0', + (bottom && !hidden) ? (height ? `${height}px` : '4.5rem') : '0', ); document.documentElement.style.setProperty( '--nav-safe-area-inset-left', - landscape ? (width ? `${width}px` : '5.5rem') : '0', + (landscape && !hidden) ? (width ? `${width}px` : '5.5rem') : '0', ); return () => { @@ -209,6 +212,7 @@ export default function NavigationBar({ }), left: 0, gap: 4, + display: hidden ? 'none' : 'flex', alignItems: 'center', backgroundColor: `color-mix(in srgb, ${theme.palette.background.body}, transparent 50%)`, backdropFilter: 'blur(10px)', diff --git a/src/navigation/useOverlayQueryParam.tsx b/src/navigation/useOverlayQueryParam.tsx new file mode 100644 index 0000000..febb350 --- /dev/null +++ b/src/navigation/useOverlayQueryParam.tsx @@ -0,0 +1,15 @@ +import { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +/** + * Custom hook that retrieves the value of the 'overlay' query parameter from the URL. + * Returns a boolean indicating whether the overlay should be hidden or not, + * the word overlay referring to the navbar, the copyright banner, and any floating elements + * that aren't the main page content. + * + * @returns {boolean} A boolean indicating whether the overlay should be hidden or not. + */ +export default function useOverlayQueryParam(): boolean { + const [urlSearchParams] = useSearchParams(); + const hidden = useMemo(() => urlSearchParams.get('overlay') === 'false', [urlSearchParams]); + return hidden; +} diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx index a9de3b4..de2e28d 100644 --- a/src/resume/Download.tsx +++ b/src/resume/Download.tsx @@ -1,22 +1,39 @@ import { Button, Card, IconButton, Stack, Tooltip, Typography, + useColorScheme, } from '@mui/joy'; import details from 'assets/Details'; import { useMobileMode } from 'components/Responsive'; import jsPDF from 'jspdf'; +import useOverlayQueryParam from 'navigation/useOverlayQueryParam'; import React, { createRef } from 'react'; import { FaRegFilePdf } from 'react-icons/fa'; import { FiDownload, FiPrinter } from 'react-icons/fi'; export default function Download() { const mobile = useMobileMode(); + const { mode, setMode } = useColorScheme(); + + const hidden = useOverlayQueryParam(); const container = createRef(); const fileName = `Resume_${details.name.first}_${details.name.last}.pdf`; const print = () => { - window.print(); + const url = new URL(window.location.href); + url.searchParams.set('overlay', 'false'); + const printWindow = window.open(url.toString(), '_blank'); + if (printWindow) { + const savedMode = mode; + setMode('light'); + printWindow.onafterprint = () => { + setMode(savedMode || 'system'); + }; + printWindow.onload = () => { + printWindow.print(); + }; + } }; const download = () => { @@ -34,6 +51,7 @@ export default function Download() { justifyContent="center" alignItems="center" sx={{ + display: hidden ? 'none' : 'flex', position: 'fixed', bottom: 'var(--nav-safe-area-inset-bottom, 0)', width: '100%', diff --git a/yarn.lock b/yarn.lock index 01537b8..4368adb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1983,11 +1983,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@juggle/resize-observer@^3.3.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" - integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== - "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -2129,33 +2124,6 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@react-hook/latest@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80" - integrity sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg== - -"@react-hook/passive-layout-effect@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" - integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== - -"@react-hook/resize-observer@^1.2.1": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz#9a8cf4c5abb09becd60d1d65f6bf10eec211e291" - integrity sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA== - dependencies: - "@juggle/resize-observer" "^3.3.1" - "@react-hook/latest" "^1.0.2" - "@react-hook/passive-layout-effect" "^1.2.0" - -"@react-hook/size@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@react-hook/size/-/size-2.1.2.tgz#87ed634ffb200f65d3e823501e5559aa3d584451" - integrity sha512-BmE5asyRDxSuQ9p14FUKJ0iBRgV9cROjqNG9jT/EjCM+xHha1HVqbPoT+14FQg1K7xIydabClCibUY4+1tw/iw== - dependencies: - "@react-hook/passive-layout-effect" "^1.2.0" - "@react-hook/resize-observer" "^1.2.1" - "@react-spring/animated@~9.7.3": version "9.7.3" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.3.tgz#4211b1a6d48da0ff474a125e93c0f460ff816e0f" From 109e3952afcd58cbeb2bf7b5e87f57e896971883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elliot=20N=C3=A9grel-Jerzy?= Date: Sat, 1 Jun 2024 16:03:00 +0200 Subject: [PATCH 05/11] refactor: improve print functionality in Download component --- src/resume/Download.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx index de2e28d..7104415 100644 --- a/src/resume/Download.tsx +++ b/src/resume/Download.tsx @@ -27,8 +27,15 @@ export default function Download() { if (printWindow) { const savedMode = mode; setMode('light'); + printWindow.onafterprint = () => { setMode(savedMode || 'system'); + printWindow.close(); + return null; + }; + printWindow.onbeforeunload = () => { + setMode(savedMode || 'system'); + return null; }; printWindow.onload = () => { printWindow.print(); @@ -40,8 +47,11 @@ export default function Download() { // eslint-disable-next-line new-cap const doc = new jsPDF(); const source = window.document.getElementById('resume') as HTMLElement; - doc.html(source); - doc.save(fileName); + doc.html(source, { + callback(d) { + d.save(fileName); + }, + }); }; return ( From ce7684924824d53c2d34575b94841792e34ae906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elliot=20N=C3=A9grel-Jerzy?= Date: Mon, 3 Jun 2024 10:31:12 +0200 Subject: [PATCH 06/11] refactor: improve print functionality in Download component --- src/resume/Download.tsx | 64 ++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/resume/Download.tsx b/src/resume/Download.tsx index 7104415..d8efed0 100644 --- a/src/resume/Download.tsx +++ b/src/resume/Download.tsx @@ -1,21 +1,22 @@ import { - Button, Card, IconButton, Stack, Tooltip, Typography, + Button, Card, CircularProgress, IconButton, Stack, Tooltip, Typography, useColorScheme, } from '@mui/joy'; import details from 'assets/Details'; import { useMobileMode } from 'components/Responsive'; import jsPDF from 'jspdf'; import useOverlayQueryParam from 'navigation/useOverlayQueryParam'; -import React, { createRef } from 'react'; +import React, { createRef, useState } from 'react'; import { FaRegFilePdf } from 'react-icons/fa'; import { FiDownload, FiPrinter } from 'react-icons/fi'; export default function Download() { const mobile = useMobileMode(); const { mode, setMode } = useColorScheme(); - const hidden = useOverlayQueryParam(); + const [downloadLoading, setDownloadLoading] = useState(false); + const container = createRef(); const fileName = `Resume_${details.name.first}_${details.name.last}.pdf`; @@ -44,14 +45,48 @@ export default function Download() { }; const download = () => { + setDownloadLoading(true); // eslint-disable-next-line new-cap - const doc = new jsPDF(); - const source = window.document.getElementById('resume') as HTMLElement; - doc.html(source, { - callback(d) { - d.save(fileName); - }, + const doc = new jsPDF({ + orientation: 'p', + format: 'a4', + unit: 'px', + hotfixes: ['px_scaling'], }); + const source = document.body; + + // Get the HTML content dimensions + const sourceWidth = source.offsetWidth; + const sourceHeight = source.offsetHeight; + + // Define the margins and PDF page dimensions + const margin = 5; + const pdfWidth = doc.internal.pageSize.getWidth() - 2 * margin; + const pdfHeight = doc.internal.pageSize.getHeight() - 2 * margin; + + // Calculate the scale factor to fit the HTML content within the PDF page + const scale = Math.min(pdfWidth / sourceWidth, pdfHeight / sourceHeight); + + // Adjust width and windowWidth to fit the scale + const adjustedWidth = sourceWidth * scale; + // const adjustedHeight = sourceHeight * scale; + const windowWidth = sourceWidth; // This can be set to the actual HTML width + + try { + doc.html(source, { + callback(d) { + d.save(fileName); + setDownloadLoading(false); + }, + x: margin, + y: margin, + autoPaging: 'text', + width: adjustedWidth, + windowWidth, + }); + } finally { + setDownloadLoading(false); + } }; return ( @@ -134,13 +169,22 @@ export default function Download() { - + {downloadLoading ? ( + + ) : ( + + )} ) : ( @@ -172,9 +175,7 @@ export default function Download() { disabled={downloadLoading} > {downloadLoading ? ( - + ) : ( )} @@ -185,9 +186,7 @@ export default function Download() { onClick={download} loading={downloadLoading} loadingPosition="start" - startDecorator={( - - )} + startDecorator={} > Download diff --git a/src/resume/Resume.tsx b/src/resume/Resume.tsx index 20e396b..ff9d01f 100644 --- a/src/resume/Resume.tsx +++ b/src/resume/Resume.tsx @@ -1,34 +1,39 @@ -/* eslint-disable react/no-danger */ import { - Avatar, Box, Button, Chip, ColorPaletteProp, Divider, Stack, Textarea, Typography, -} from '@mui/joy'; -import React, { useMemo, useState } from 'react'; -import { - RiBriefcaseLine, -} from 'react-icons/ri'; -import { TbSchool } from 'react-icons/tb'; -import { IoLanguage } from 'react-icons/io5'; -import details from 'assets/Details'; -import { Education, Experience } from 'components/Details'; -import Meta from 'components/Meta'; -import { useMobileMode } from 'components/Responsive'; -import { marked } from 'marked'; + Avatar, + Box, + Button, + Chip, + ColorPaletteProp, + Divider, + Stack, + Textarea, + Typography, +} from "@mui/joy"; +import { useMemo, useState } from "react"; +import { RiBriefcaseLine } from "react-icons/ri"; +import { TbSchool } from "react-icons/tb"; +import { IoLanguage } from "react-icons/io5"; +import details from "@/assets/Details"; +import { Education, Experience } from "@/components/Details"; +import Meta from "@/components/Meta"; +import { useMobileMode } from "@/components/Responsive"; +import { marked } from "marked"; export function Languages() { const color = (level: string): ColorPaletteProp => { switch (level) { - case 'A1': - case 'A2': - case 'B1': - return 'neutral'; - case 'B2': - return 'info'; - case 'C1': - return 'primary'; - case 'C2': - return 'success'; + case "A1": + case "A2": + case "B1": + return "neutral"; + case "B2": + return "info"; + case "C1": + return "primary"; + case "C2": + return "success"; default: - return 'info'; + return "info"; } }; @@ -40,7 +45,7 @@ export function Languages() { key={language.name} color={color(language.level)} variant="outlined" - startDecorator={( + startDecorator={ {language.level} - )} + } > - {`${language.name}${language.native ? ' (native)' : ''}`} + {`${language.name}${language.native ? " (native)" : ""}`} ))} @@ -62,23 +67,30 @@ export default function Resume() { const mobile = useMobileMode(); const [descriptionEditable, setDescriptionEditable] = useState(false); - const [descriptionContent, setDescriptionContent] = useState('Skilled **full-stack developer** with expertise in diverse programming languages and frameworks. Proven ability to deliver impactful projects on GitHub, fostering a **collaborative environment**. Adept at tackling **complex challenges** and thriving in team settings. Seeking to leverage skills in a dynamic role.'); + const [descriptionContent, setDescriptionContent] = useState( + "Skilled **full-stack developer** with expertise in diverse programming languages and frameworks. Proven ability to deliver impactful projects on GitHub, fostering a **collaborative environment**. Adept at tackling **complex challenges** and thriving in team settings. Seeking to leverage skills in a dynamic role." + ); const parsedDescriptionContent = useMemo( - () => (marked.parse(descriptionContent) as string).replace(/

/g, '').replace(/<\/p>/g, ''), - [descriptionContent], + () => + (marked.parse(descriptionContent) as string) + .replace(/

/g, "") + .replace(/<\/p>/g, ""), + [descriptionContent] ); return ( <> - + @@ -93,7 +105,12 @@ export default function Resume() { {`${details.name.first} ${details.name.last}`} - + Software Engineer {descriptionEditable ? ( @@ -104,10 +121,10 @@ export default function Resume() { setDescriptionContent(event.target.value); }} sx={{ - padding: '.2rem .5rem', - margin: '-.2rem -.5rem', - marginTop: '.2rem', - fontSize: 'var(--joy-fontSize-sm)', + padding: ".2rem .5rem", + margin: "-.2rem -.5rem", + marginTop: ".2rem", + fontSize: "var(--joy-fontSize-sm)", }} /> @@ -130,20 +147,24 @@ export default function Resume() { }} level="body2" sx={{ - position: 'relative', - borderRadius: '.5rem', - padding: '.2rem .5rem', - margin: '-.2rem -.5rem', - marginTop: '.2rem', - outline: '1px solid transparent', - transition: 'all ease .2s', - cursor: 'pointer', - '&:hover': { - outlineColor: 'var(--joy-palette-divider)', + position: "relative", + borderRadius: ".5rem", + padding: ".2rem .5rem", + margin: "-.2rem -.5rem", + marginTop: ".2rem", + outline: "1px solid transparent", + transition: "all ease .2s", + cursor: "pointer", + "&:hover": { + outlineColor: "var(--joy-palette-divider)", }, }} > -

+
)} @@ -151,71 +172,68 @@ export default function Resume() { component="section" sx={{ gap: 3, - display: 'flex', - flexDirection: 'column', + display: "flex", + flexDirection: "column", }} > {[ - { key: 'address', label: 'Address' }, - { key: 'email', label: 'Email' }, - { key: 'phone', label: 'Phone' }, - { key: 'website', label: 'Website' }, - ].map( - ({ key, label }) => { - const value = details.contact[ - key as keyof typeof details.contact - ]; - const isUrl = value.startsWith('http'); - return ( - + { key: "address", label: "Address" }, + { key: "email", label: "Email" }, + { key: "phone", label: "Phone" }, + { key: "website", label: "Website" }, + ].map(({ key, label }) => { + const value = + details.contact[key as keyof typeof details.contact]; + const isUrl = value.startsWith("http"); + return ( + + + {label} + + {isUrl ? ( + + {value} + + ) : ( - {label} + {value} - {isUrl ? ( - - {value} - - ) : ( - - {value} - - )} - - ); - }, - )} + )} + + ); + })} - )} + } > Work experience @@ -227,11 +245,11 @@ export default function Resume() { - )} + } > Education @@ -245,11 +263,11 @@ export default function Resume() { - )} + } > Languages From 1b84e315cd201fdbf7e332a45306fbc7ded21087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elliot=20N=C3=A9grel-Jerzy?= Date: Sun, 18 Aug 2024 18:13:11 +0200 Subject: [PATCH 09/11] feat: add Download component to Resume page --- src/components/projects/Projects.tsx | 258 ++++------------ .../projects/ThemeAwareIllustration.tsx | 92 ++++++ .../projects/ThemeSwitcherButton.tsx | 74 +++++ src/{ => components}/resume/Download.tsx | 17 +- src/pages/Resume.tsx | 2 + src/resume/Resume.tsx | 285 ------------------ 6 files changed, 229 insertions(+), 499 deletions(-) create mode 100644 src/components/projects/ThemeAwareIllustration.tsx create mode 100644 src/components/projects/ThemeSwitcherButton.tsx rename src/{ => components}/resume/Download.tsx (95%) delete mode 100644 src/resume/Resume.tsx diff --git a/src/components/projects/Projects.tsx b/src/components/projects/Projects.tsx index 8388064..3816826 100644 --- a/src/components/projects/Projects.tsx +++ b/src/components/projects/Projects.tsx @@ -1,173 +1,13 @@ -import { - Button, IconButton, Stack, Typography, - useColorScheme, -} from '@mui/joy'; -import { animated, useSpringRef, useTransition } from '@react-spring/web'; -import architectureDarkMin from '@/assets/architecture_dark.min.webp'; -import architectureDark from '@/assets/architecture_dark.webp'; -import architectureLightMin from '@/assets/architecture_light.min.webp'; -import architectureLight from '@/assets/architecture_light.webp'; -import ProgressiveImage from '@/components/ProgressiveImage'; -import { - useMobileMode, -} from '@/components/Responsive'; -import Meta from '@/components/Meta'; -import React, { useEffect, useState } from 'react'; -import { GoMoon, GoSun } from 'react-icons/go'; -import { IoIosReturnLeft } from 'react-icons/io'; -import { Link } from 'react-router-dom'; -import details from '@/assets/Details'; -import Directory from '@/components/projects/Directory'; +import { Button, Stack, Typography } from "@mui/joy"; -function ThemeSwitcherButton() { - const { colorScheme, setMode } = useColorScheme(); - - return ( - { - setMode((colorScheme) === 'light' ? 'dark' : 'light'); - }} - sx={(theme) => ({ - transition: 'all ease .2s', - position: 'relative', - borderRadius: '0', - width: 'fit-content', - flexShrink: 0, - padding: '1 2', - overflow: 'hidden', - background: theme.palette.background.body, - - '& > svg': { - transition: 'all ease .2s', - }, - - '&:hover': { - background: theme.palette.text.primary, - color: theme.palette.background.level1, - borderColor: theme.palette.text.primary, - '& > svg': { - transform: 'rotate(-45deg)', - }, - }, - '&:active': { - transform: 'scale(.98)', - }, - - '& > div': { - transition: 'all ease .2s', - }, - - '&.state-light > div': { - transform: 'translate(-50%, calc(-50% - 21px))', - }, - - '&.state-dark > div': { - transform: 'translate(-50%, calc(-50% + 21px))', - }, - - '&:hover > div': { - transform: 'translate(-50%, -50%)', - }, - })} - > - - - - - - ); -} - -function ThemeAwareIllustration() { - const { colorScheme } = useColorScheme(); - const [loaded, setLoaded] = useState(false); - - const transRef = useSpringRef(); - - const transitions = useTransition((colorScheme), { - ref: transRef, - keys: null, - from: { opacity: 0 }, - enter: { opacity: 1 }, - leave: { opacity: 0, filter: 'blur(10px)', position: 'absolute' }, - }); - - useEffect(() => { - if (loaded) { transRef.start(); } - }, [colorScheme, loaded]); - - const imgSx = { - position: 'relative', - marginTop: '-23rem', - width: '100%', - WebkitMaskImage: 'linear-gradient(to left,black 10%,transparent 80%)', - maskImage: 'linear-gradient(to left,black 10%,transparent 80%)', - filter: 'grayscale(1)', - } as React.CSSProperties; - - return ( - - {transitions((style, item) => { - switch (item) { - case 'light': - return ( - - setLoaded(true)} - style={imgSx} - /> - - ); - case 'dark': - return ( - - setLoaded(true)} - style={imgSx} - /> - - ); - default: - return null; - } - })} - - ); -} +import { useMobileMode } from "@/components/Responsive"; +import Meta from "@/components/Meta"; +import { IoIosReturnLeft } from "react-icons/io"; +import { Link } from "react-router-dom"; +import details from "@/assets/Details"; +import Directory from "@/components/projects/Directory"; +import { ThemeAwareIllustration } from "./ThemeAwareIllustration"; +import { ThemeSwitcherButton } from "./ThemeSwitcherButton"; export default function Projects() { const mobile = useMobileMode(); @@ -177,33 +17,35 @@ export default function Projects() { position="relative" overflow="hidden" sx={{ - paddingLeft: 'var(--nav-safe-area-inset-left)', - paddingBottom: 'var(--nav-safe-area-inset-bottom)', + paddingLeft: "var(--nav-safe-area-inset-left)", + paddingBottom: "var(--nav-safe-area-inset-bottom)", }} > - + - - + + Featured @@ -224,12 +66,16 @@ export default function Projects() { *:first-child': { - flex: 1, - }, - } : {}} + sx={ + mobile + ? { + width: "100%", + "& > *:first-child": { + flex: 1, + }, + } + : {} + } >