✨ feat(card): add Business Card page

adds a page with print functionality
This commit is contained in:
2025-07-03 10:32:14 +02:00
parent 76575ead15
commit 8ba9e78a16
7 changed files with 596 additions and 0 deletions
+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 ContactPage = React.lazy(() => import('components/pages/ContactPage/index.js'))
const PrivacyPage = React.lazy(() => import('components/pages/PrivacyPage/index.js'))
const BusinessCardPage = React.lazy(() => import('components/pages/BusinessCardPage/index.js'))
const router = createBrowserRouter([
{
@@ -57,6 +58,14 @@ const router = createBrowserRouter([
</React.Suspense>
),
},
{
path: 'card',
element: (
<React.Suspense fallback={<Loading />}>
<BusinessCardPage />
</React.Suspense>
),
},
],
},
])
@@ -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,192 @@
import React from 'react'
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 './index.css'
const NAME = import.meta.env.VITE_BC_NAME || '[FULL NAME]'
const POSITION = import.meta.env.VITE_BC_POSITION || '[POSITION]'
const VAT = import.meta.env.VITE_BC_VAT || '[VAT NUMBER]'
const PHONE = import.meta.env.VITE_BC_PHONE || '[Redacted for privacy]'
const EMAIL = import.meta.env.VITE_BC_EMAIL || '[Redacted for privacy]'
const LINKEDIN = import.meta.env.VITE_BC_LINKEDIN || '[Redacted for privacy]'
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',
'-webkit-print-color-adjust': 'exact !important',
'print-color-adjust': '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}>
{NAME}
</Typography>
<Typography variant="subtitle2" color="text.secondary" sx={{ fontSize: '0.55rem', mb: 0.5 }}>
{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}>
{NAME}
</Typography>
<Typography variant="caption" color="text.secondary" fontSize="0.55rem" mb={0.5}>
{VAT}
</Typography>
<TypographyLink icon={PhoneInTalk} href={`tel:${PHONE.replace(/[^\d+]/g, '').replace(/^\+?/, '+')}`}>
{PHONE}
</TypographyLink>
<TypographyLink icon={Email} href={`mailto:${EMAIL}`}>
{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/${LINKEDIN}/`}>
{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
View File
@@ -2,5 +2,6 @@ export { default as MainPage } from './MainPage'
export { default as ContactPage } from './ContactPage'
export { default as CurriculumPage } from './CurriculumPage'
export { default as ProjectsPage } from './ProjectsPage'
export { default as BusinessCardPage } from './BusinessCardPage'
export { default as ErrorPage } from './ErrorPage'
+6
View File
@@ -2,6 +2,12 @@
interface ImportMetaEnv {
readonly VITE_GA_MEASUREMENT_ID: string
readonly VITE_BC_NAME: string
readonly VITE_BC_POSITION: string
readonly VITE_BC_VAT: string
readonly VITE_BC_PHONE: string
readonly VITE_BC_EMAIL: string
readonly VITE_BC_LINKEDIN: string
}
interface ImportMeta {