feat: resume
This commit is contained in:
parent
a60f3cf0b5
commit
139bc201b8
@ -10,7 +10,10 @@ import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import TypingAnimation from '@/components/TypingAnimation';
|
||||
import ProjectMasonry from '@/components/ProjectMasonry';
|
||||
import SocialIcons from '@/components/SocialIcons';
|
||||
import Timeline from '@/components/Timeline/Timeline';
|
||||
import projects from '@/data/projects';
|
||||
import timelineData from '@/data/timeline';
|
||||
|
||||
export default function HomePage() {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
@ -32,6 +35,9 @@ export default function HomePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Social Icons */}
|
||||
{showContent && <SocialIcons />}
|
||||
|
||||
<Fade in={showContent} timeout={1000} style={{ transitionDelay: showContent ? '500ms' : '0ms' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
@ -61,7 +67,15 @@ export default function HomePage() {
|
||||
>
|
||||
d4m13n
|
||||
</Box>
|
||||
<Box component="span">.dev</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
textShadow: '0 0 10px rgba(255, 255, 255, 0.25), 0 0 20px rgba(255, 255, 255, 0.15)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))'
|
||||
}}
|
||||
>
|
||||
.dev
|
||||
</Box>
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '100%', mb: 4 }}>
|
||||
@ -88,8 +102,25 @@ export default function HomePage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab label="Software Dev Projects" />
|
||||
<Tab label="Game Dev Projects" />
|
||||
<Tab label={
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
<Box sx={{ display: { xs: 'block', sm: 'none' } }}>Software</Box>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>Software Dev Projects</Box>
|
||||
</Box>
|
||||
} />
|
||||
<Tab label={
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
<Box sx={{ display: { xs: 'block', sm: 'none' } }}>Game Dev</Box>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>Game Dev Projects</Box>
|
||||
</Box>
|
||||
} />
|
||||
<Tab label="Resume" />
|
||||
<Tab label="About" />
|
||||
<Tab label="Contact" />
|
||||
</Tabs>
|
||||
@ -106,7 +137,14 @@ export default function HomePage() {
|
||||
<ProjectMasonry projects={projects} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTab === 2 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 1200, mx: 'auto', p: 2 }}>
|
||||
<Timeline items={timelineData} orientation="vertical" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTab === 3 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 800, mx: 'auto', p: 2 }}>
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mb: 2 }}>
|
||||
About Me
|
||||
@ -122,7 +160,7 @@ export default function HomePage() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTab === 3 && (
|
||||
{activeTab === 4 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 800, mx: 'auto', p: 2 }}>
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mb: 2 }}>
|
||||
Contact
|
||||
|
@ -223,13 +223,16 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
label={tech}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.7)', // Red background
|
||||
color: textColor,
|
||||
height: 24,
|
||||
fontSize: '0.75rem',
|
||||
boxShadow: '0 0 10px rgba(255, 0, 0, 0.5), 0 0 20px rgba(255, 0, 0, 0.3)', // Red glow
|
||||
filter: 'drop-shadow(0 2px 4px rgba(255, 0, 0, 0.4))',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
boxShadow: ambientGlow,
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.8)', // Slightly darker red on hover
|
||||
boxShadow: '0 0 15px rgba(255, 0, 0, 0.6), 0 0 30px rgba(255, 0, 0, 0.4)', // Enhanced red glow
|
||||
filter: 'drop-shadow(0 4px 8px rgba(255, 0, 0, 0.5))',
|
||||
},
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
}}
|
||||
@ -265,10 +268,12 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
borderRadius: '6px',
|
||||
padding: '4px 12px',
|
||||
minWidth: '80px',
|
||||
boxShadow: '0 0 5px rgba(10, 25, 50, 0.3), 0 0 10px rgba(10, 25, 50, 0.2)',
|
||||
'&:hover': {
|
||||
borderColor: textColor,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
boxShadow: ambientGlow,
|
||||
borderColor: '#4fd1ff',
|
||||
backgroundColor: 'rgba(79, 209, 255, 0.1)',
|
||||
boxShadow: '0 0 10px rgba(10, 25, 50, 0.5), 0 0 20px rgba(10, 25, 50, 0.4)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(10, 25, 50, 0.5))',
|
||||
},
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
}}
|
||||
@ -294,9 +299,12 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
borderRadius: '6px',
|
||||
padding: '4px 12px',
|
||||
minWidth: '80px',
|
||||
boxShadow: '0 0 10px rgba(10, 25, 50, 0.5), 0 0 20px rgba(10, 25, 50, 0.4)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(10, 25, 50, 0.5))',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(79, 209, 255, 0.4)',
|
||||
boxShadow: enhancedGlow,
|
||||
backgroundColor: 'rgba(79, 209, 255, 0.5)',
|
||||
boxShadow: '0 0 15px rgba(10, 25, 50, 0.6), 0 0 30px rgba(10, 25, 50, 0.5)',
|
||||
filter: 'drop-shadow(0 4px 8px rgba(10, 25, 50, 0.6))',
|
||||
},
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
}}
|
||||
|
169
src/components/SocialIcons.tsx
Normal file
169
src/components/SocialIcons.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { Box, IconButton, Tooltip, SvgIcon } from '@mui/material';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import LinkedInIcon from '@mui/icons-material/LinkedIn';
|
||||
import { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
// Custom icons for platforms without MUI icons
|
||||
const GiteaIcon = (props: SvgIconProps) => (
|
||||
|
||||
<SvgIcon {...props} viewBox="0 0 512 512">
|
||||
<rect rx="15%" height="512" width="512" />
|
||||
<path d="M419 150c-98 7-186 2-276-1-27 0-63 19-61 67 3 75 71 82 99 83 3 14 35 62 59 65h104c63-5 109-213 75-214zm-311 67c-3-21 7-42 42-42 3 39 10 61 22 96-32-5-59-15-64-54z" fill="#592" />
|
||||
<path d="m293 152v70" strokeWidth="9" />
|
||||
<g transform="rotate(25.7 496 -423)" strokeWidth="7" fill="#592">
|
||||
<path d="M561 246h97" />
|
||||
<rect x="561" y="246" width="97" height="97" rx="16" />
|
||||
<path d="M592 245v75" />
|
||||
<path d="M592 273c45 0 38-5 38 48" fill="none" />
|
||||
<circle cx="592" cy="320" r="10" />
|
||||
<circle cx="630" cy="320" r="10" />
|
||||
<circle cx="592" cy="273" r="10" />
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
const DiscordIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
const XIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
const FilesIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
const AIIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5-9h10v2H7z" />
|
||||
<path d="M10 7H8v6h2zm6 0h-2v6h2z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
// Interface for social media links
|
||||
export interface SocialLink {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: React.ElementType;
|
||||
}
|
||||
|
||||
// Default social links
|
||||
const defaultSocialLinks: SocialLink[] = [
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
url: 'https://www.linkedin.com/in/damien-ostler-254663110/',
|
||||
icon: LinkedInIcon,
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: 'https://github.com/d4m13n-d3v',
|
||||
icon: GitHubIcon,
|
||||
},
|
||||
{
|
||||
name: 'Gitea',
|
||||
url: 'https://git.d4m13n.dev',
|
||||
icon: GiteaIcon,
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
url: 'https://discord.gg/8dHnaarghJ',
|
||||
icon: DiscordIcon,
|
||||
},
|
||||
{
|
||||
name: 'X',
|
||||
url: 'https://x.com/d4m13n_d3v',
|
||||
icon: XIcon,
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
url: 'https://files.d4m13n.dev',
|
||||
icon: FilesIcon,
|
||||
},
|
||||
{
|
||||
name: 'AI',
|
||||
url: 'https://ai.d4m13n.dev',
|
||||
icon: AIIcon,
|
||||
},
|
||||
];
|
||||
|
||||
// Glow effects
|
||||
const whiteGlowEffects = {
|
||||
textShadow: '0 0 10px rgba(255, 255, 255, 0.25), 0 0 20px rgba(255, 255, 255, 0.15)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))'
|
||||
};
|
||||
|
||||
const blueGlowEffects = {
|
||||
textShadow: '0 0 10px rgba(79, 209, 255, 0.15), 0 0 20px rgba(79, 209, 255, 0.1)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))'
|
||||
};
|
||||
|
||||
interface SocialIconsProps {
|
||||
links?: SocialLink[];
|
||||
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
||||
}
|
||||
|
||||
const SocialIcons: React.FC<SocialIconsProps> = ({
|
||||
links = defaultSocialLinks,
|
||||
position = 'top-right'
|
||||
}) => {
|
||||
// Determine position styling
|
||||
const getPositionStyling = () => {
|
||||
switch (position) {
|
||||
case 'top-right':
|
||||
return { top: 16, right: 16 };
|
||||
case 'top-left':
|
||||
return { top: 16, left: 16 };
|
||||
case 'bottom-right':
|
||||
return { bottom: 16, right: 16 };
|
||||
case 'bottom-left':
|
||||
return { bottom: 16, left: 16 };
|
||||
default:
|
||||
return { top: 16, right: 16 };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
zIndex: 1000,
|
||||
display: { xs: 'none', sm: 'flex' }, // Hide on mobile (xs), show on sm and up
|
||||
gap: 1,
|
||||
...getPositionStyling()
|
||||
}}
|
||||
>
|
||||
{links.map((link) => (
|
||||
<Tooltip key={link.name} title={link.name} arrow>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'white',
|
||||
...whiteGlowEffects,
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
color: '#4fd1ff',
|
||||
...blueGlowEffects
|
||||
}
|
||||
}}
|
||||
>
|
||||
<link.icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialIcons;
|
99
src/components/Timeline/MasonryGrid.tsx
Normal file
99
src/components/Timeline/MasonryGrid.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { MasonryGridProps } from './types';
|
||||
import MasonryItem from './MasonryItem';
|
||||
|
||||
const MasonryGrid: React.FC<MasonryGridProps> = ({
|
||||
items,
|
||||
isVisible = true,
|
||||
animationDelay = 0
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||
|
||||
const [visibleItems, setVisibleItems] = useState<boolean[]>(
|
||||
Array(items.length).fill(false)
|
||||
);
|
||||
|
||||
// Staggered animation for items
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
const timers: NodeJS.Timeout[] = [];
|
||||
|
||||
items.forEach((_, index) => {
|
||||
const timer = setTimeout(() => {
|
||||
setVisibleItems(prev => {
|
||||
const newState = [...prev];
|
||||
newState[index] = true;
|
||||
return newState;
|
||||
});
|
||||
}, animationDelay + (index * 150)); // Stagger each item by 150ms
|
||||
|
||||
timers.push(timer);
|
||||
});
|
||||
|
||||
return () => timers.forEach(timer => clearTimeout(timer));
|
||||
} else {
|
||||
setVisibleItems(Array(items.length).fill(false));
|
||||
}
|
||||
}, [isVisible, items.length, animationDelay]);
|
||||
|
||||
// Determine column count based on screen size
|
||||
const getColumnCount = () => {
|
||||
if (isMobile) return 1;
|
||||
if (isTablet) return 2;
|
||||
return 3;
|
||||
};
|
||||
|
||||
// Distribute items into columns for masonry layout
|
||||
const getColumns = () => {
|
||||
const columnCount = getColumnCount();
|
||||
const columns: React.ReactNode[][] = Array.from({ length: columnCount }, () => []);
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const columnIndex = index % columnCount;
|
||||
columns[columnIndex].push(
|
||||
<MasonryItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isVisible={visibleItems[index]}
|
||||
animationDelay={0} // Already handled by parent staggering
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
const columns = getColumns();
|
||||
const columnCount = getColumnCount();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<Box
|
||||
key={`column-${index}`}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: `calc(${100 / columnCount}% - ${(columnCount - 1) * 8 / columnCount}px)`,
|
||||
}}
|
||||
>
|
||||
{column}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasonryGrid;
|
117
src/components/Timeline/MasonryItem.tsx
Normal file
117
src/components/Timeline/MasonryItem.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import { Box, Card, CardContent, CardMedia, Typography, Fade } from '@mui/material';
|
||||
import { MasonryItemProps } from './types';
|
||||
|
||||
const MasonryItem: React.FC<MasonryItemProps> = ({
|
||||
item,
|
||||
isVisible = true,
|
||||
animationDelay = 0
|
||||
}) => {
|
||||
// Define colors and effects
|
||||
const redBackground = 'rgba(244, 67, 54, 0.4)'; // Red background similar to exclamation mark
|
||||
const titleColor = '#f44336'; // Material UI red (same as exclamation mark)
|
||||
const titleGlow = '0 0 10px rgba(244, 67, 54, 0.35), 0 0 20px rgba(244, 67, 54, 0.25)'; // Same glow as exclamation mark
|
||||
|
||||
// Enhanced shadow effects with red glow like the exclamation mark
|
||||
const shadowEffects = {
|
||||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2), 0 0 10px rgba(244, 67, 54, 0.35), 0 0 20px rgba(244, 67, 54, 0.25)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(244, 67, 54, 0.3))',
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
boxShadow: '0 6px 12px rgba(0, 0, 0, 0.3), 0 0 15px rgba(244, 67, 54, 0.5), 0 0 30px rgba(244, 67, 54, 0.35)',
|
||||
filter: 'drop-shadow(0 4px 8px rgba(244, 67, 54, 0.4))',
|
||||
transform: 'translateY(-5px)'
|
||||
}
|
||||
};
|
||||
|
||||
// Render different content based on the item type
|
||||
const renderContent = () => {
|
||||
switch (item.type) {
|
||||
case 'image':
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
height: item.height || 'auto',
|
||||
width: '100%',
|
||||
backgroundColor: redBackground,
|
||||
...shadowEffects
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={item.imageUrl || 'https://via.placeholder.com/300'}
|
||||
alt={item.content}
|
||||
sx={{
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'card':
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
height: item.height || 'auto',
|
||||
width: '100%',
|
||||
backgroundColor: redBackground,
|
||||
...shadowEffects
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="body1">{item.content}</Typography>
|
||||
</CardContent>
|
||||
{item.imageUrl && (
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={item.imageUrl}
|
||||
alt="Card image"
|
||||
sx={{ height: 140 }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
height: item.height || 'auto',
|
||||
width: '100%',
|
||||
backgroundColor: redBackground,
|
||||
borderRadius: 1,
|
||||
...shadowEffects
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{item.content}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fade
|
||||
in={isVisible}
|
||||
timeout={800}
|
||||
style={{
|
||||
transitionDelay: `${animationDelay}ms`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 2,
|
||||
width: '100%', // Always take full width
|
||||
height: item.height || 'auto',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{renderContent()}
|
||||
</Box>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasonryItem;
|
106
src/components/Timeline/Timeline.tsx
Normal file
106
src/components/Timeline/Timeline.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { TimelineProps, TimelineOrientation } from './types';
|
||||
import TimelineItem from './TimelineItem';
|
||||
|
||||
const Timeline: React.FC<TimelineProps> = ({
|
||||
items,
|
||||
orientation = 'vertical',
|
||||
className
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Force vertical orientation on mobile
|
||||
const effectiveOrientation: TimelineOrientation = isMobile ? 'vertical' : orientation;
|
||||
|
||||
// Track which items are visible based on scroll position
|
||||
const [visibleItems, setVisibleItems] = useState<boolean[]>(
|
||||
Array(items.length).fill(false)
|
||||
);
|
||||
|
||||
// Handle scroll-based animations
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (timelineRef.current) {
|
||||
const timelineRect = timelineRef.current.getBoundingClientRect();
|
||||
const timelineItems = timelineRef.current.querySelectorAll('[data-timeline-item]');
|
||||
|
||||
timelineItems.forEach((item, index) => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
const isVisible =
|
||||
rect.top <= window.innerHeight * 0.8 &&
|
||||
rect.bottom >= window.innerHeight * 0.2;
|
||||
|
||||
setVisibleItems(prev => {
|
||||
if (prev[index] !== isVisible) {
|
||||
const newState = [...prev];
|
||||
newState[index] = isVisible;
|
||||
return newState;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
handleScroll();
|
||||
|
||||
// Add scroll listener
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [items.length]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={timelineRef}
|
||||
className={className}
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: effectiveOrientation === 'vertical' ? 'column' : 'row',
|
||||
alignItems: effectiveOrientation === 'vertical' ? 'stretch' : 'flex-start',
|
||||
overflowX: effectiveOrientation === 'horizontal' ? 'auto' : 'visible',
|
||||
overflowY: 'visible',
|
||||
p: 2,
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': {
|
||||
height: 8,
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
data-timeline-item
|
||||
sx={{
|
||||
...(effectiveOrientation === 'horizontal' && {
|
||||
minWidth: 300,
|
||||
maxWidth: 400,
|
||||
mr: index < items.length - 1 ? 4 : 0,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TimelineItem
|
||||
item={item}
|
||||
orientation={effectiveOrientation}
|
||||
isVisible={visibleItems[index]}
|
||||
index={index}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
239
src/components/Timeline/TimelineItem.tsx
Normal file
239
src/components/Timeline/TimelineItem.tsx
Normal file
@ -0,0 +1,239 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Fade,
|
||||
Paper,
|
||||
useTheme,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { TimelineItemProps } from './types';
|
||||
import MasonryGrid from './MasonryGrid';
|
||||
|
||||
const TimelineItem: React.FC<TimelineItemProps> = ({
|
||||
item,
|
||||
orientation,
|
||||
isVisible = true,
|
||||
index
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// Force vertical orientation on mobile
|
||||
const effectiveOrientation = isMobile ? 'vertical' : orientation;
|
||||
|
||||
// Determine if this item should be on the left or right (for vertical orientation)
|
||||
// or top or bottom (for horizontal orientation)
|
||||
const isAlternating = index % 2 === 1;
|
||||
|
||||
// Enhanced glow effects for the marker - using deep dark blue with stronger glow
|
||||
const markerGlowEffects = {
|
||||
boxShadow: isHovered
|
||||
? '0 0 15px rgba(10, 25, 50, 0.6), 0 0 30px rgba(10, 25, 50, 0.5)'
|
||||
: '0 0 10px rgba(10, 25, 50, 0.5), 0 0 20px rgba(10, 25, 50, 0.4)',
|
||||
filter: isHovered
|
||||
? 'drop-shadow(0 4px 8px rgba(10, 25, 50, 0.6))'
|
||||
: 'drop-shadow(0 2px 4px rgba(10, 25, 50, 0.5))',
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
};
|
||||
|
||||
// Subtle shadow for the content without white glow
|
||||
const contentShadowEffects = {
|
||||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)',
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
};
|
||||
|
||||
// Enhanced button hover effects - using darker blue glow
|
||||
const buttonHoverEffects = {
|
||||
boxShadow: '0 0 10px rgba(10, 25, 50, 0.5), 0 0 20px rgba(10, 25, 50, 0.4)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(10, 25, 50, 0.5))',
|
||||
'&:hover': {
|
||||
transform: 'translateX(5px)',
|
||||
boxShadow: '0 0 15px rgba(10, 25, 50, 0.6), 0 0 30px rgba(10, 25, 50, 0.5)',
|
||||
filter: 'drop-shadow(0 4px 8px rgba(10, 25, 50, 0.6))',
|
||||
},
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
};
|
||||
|
||||
// Render the timeline marker
|
||||
const renderMarker = () => (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: '4px', // Square with slightly rounded corners
|
||||
backgroundColor: '#0a1932', // Deep dark blue
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...markerGlowEffects,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 'bold' }}>
|
||||
{item.date}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Line connecting to the next item */}
|
||||
{effectiveOrientation === 'vertical' ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: 3,
|
||||
height: '100%',
|
||||
backgroundColor: '#0a1932', // Deep dark blue
|
||||
opacity: 0.5,
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: 3,
|
||||
width: '100%',
|
||||
backgroundColor: '#0a1932', // Deep dark blue
|
||||
opacity: 0.5,
|
||||
ml: 1,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Render the content section
|
||||
const renderContent = () => (
|
||||
<Fade
|
||||
in={isVisible}
|
||||
timeout={800}
|
||||
style={{
|
||||
transitionDelay: `${index * 200}ms`,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
width: '100%',
|
||||
...contentShadowEffects,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" paragraph>
|
||||
{item.description}
|
||||
</Typography>
|
||||
|
||||
<MasonryGrid
|
||||
items={item.items}
|
||||
isVisible={isVisible}
|
||||
animationDelay={index * 200 + 200} // Delay after the content fades in
|
||||
/>
|
||||
|
||||
{item.actionUrl && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
endIcon={<ArrowForwardIcon />}
|
||||
href={item.actionUrl}
|
||||
sx={{
|
||||
backgroundColor: '#0a1932', // Deep dark blue
|
||||
color: 'white', // White text
|
||||
...buttonHoverEffects,
|
||||
'&:hover': {
|
||||
...buttonHoverEffects['&:hover'],
|
||||
backgroundColor: '#152a45', // Slightly lighter on hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.actionText || 'Go to'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Fade>
|
||||
);
|
||||
|
||||
// Layout for vertical orientation
|
||||
if (effectiveOrientation === 'vertical') {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
mb: 4,
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* For alternating layout on desktop */}
|
||||
{!isMobile && isAlternating ? (
|
||||
<>
|
||||
<Box sx={{ flex: 1 }}>{renderContent()}</Box>
|
||||
<Box sx={{ width: 80, display: 'flex', justifyContent: 'center' }}>
|
||||
{renderMarker()}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }} /> {/* Empty space for alignment */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ flex: 1 }} /> {/* Empty space for alignment */}
|
||||
<Box sx={{ width: 80, display: 'flex', justifyContent: 'center' }}>
|
||||
{renderMarker()}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>{renderContent()}</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Layout for horizontal orientation
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minWidth: 300,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{isAlternating ? (
|
||||
<>
|
||||
<Box sx={{ height: 80, display: 'flex', alignItems: 'center' }}>
|
||||
{renderMarker()}
|
||||
</Box>
|
||||
<Box sx={{ width: '100%' }}>{renderContent()}</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ width: '100%' }}>{renderContent()}</Box>
|
||||
<Box sx={{ height: 80, display: 'flex', alignItems: 'center' }}>
|
||||
{renderMarker()}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
5
src/components/Timeline/index.ts
Normal file
5
src/components/Timeline/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as Timeline } from './Timeline';
|
||||
export { default as TimelineItem } from './TimelineItem';
|
||||
export { default as MasonryGrid } from './MasonryGrid';
|
||||
export { default as MasonryItem } from './MasonryItem';
|
||||
export * from './types';
|
47
src/components/Timeline/types.ts
Normal file
47
src/components/Timeline/types.ts
Normal file
@ -0,0 +1,47 @@
|
||||
export interface TimelineItemData {
|
||||
id: string;
|
||||
date: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actionUrl?: string;
|
||||
actionText?: string;
|
||||
items: MasonryItemData[];
|
||||
}
|
||||
|
||||
export interface MasonryItemData {
|
||||
id: string;
|
||||
type: 'image' | 'card' | 'text';
|
||||
content: string;
|
||||
title?: string; // Optional title for the masonry item
|
||||
imageUrl?: string;
|
||||
width?: number; // For controlling the size in the masonry grid
|
||||
height?: number;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export type TimelineOrientation = 'horizontal' | 'vertical';
|
||||
|
||||
export interface TimelineProps {
|
||||
items: TimelineItemData[];
|
||||
orientation?: TimelineOrientation;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TimelineItemProps {
|
||||
item: TimelineItemData;
|
||||
orientation: TimelineOrientation;
|
||||
isVisible?: boolean;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface MasonryGridProps {
|
||||
items: MasonryItemData[];
|
||||
isVisible?: boolean;
|
||||
animationDelay?: number;
|
||||
}
|
||||
|
||||
export interface MasonryItemProps {
|
||||
item: MasonryItemData;
|
||||
isVisible?: boolean;
|
||||
animationDelay?: number;
|
||||
}
|
155
src/data/timeline.ts
Normal file
155
src/data/timeline.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { TimelineItemData } from '@/components/Timeline/types';
|
||||
|
||||
// Timeline data representing career/project milestones
|
||||
const timelineData: TimelineItemData[] = [
|
||||
{
|
||||
id: 'timeline-1',
|
||||
date: '2023',
|
||||
title: 'Full Stack Developer',
|
||||
description: 'Led development of a comprehensive e-commerce platform with advanced product management, shopping cart functionality, and secure payment processing.',
|
||||
actionUrl: 'https://github.com/username/ecommerce-platform',
|
||||
actionText: 'View Project',
|
||||
items: [
|
||||
{
|
||||
id: 'masonry-1-1',
|
||||
type: 'image',
|
||||
content: 'E-commerce dashboard',
|
||||
imageUrl: 'https://source.unsplash.com/random/800x600?ecommerce',
|
||||
width: 100,
|
||||
height: 200
|
||||
},
|
||||
{
|
||||
id: 'masonry-1-2',
|
||||
type: 'card',
|
||||
title: 'Payment Processing',
|
||||
content: 'Implemented secure payment processing with Stripe integration, supporting multiple payment methods and currencies.',
|
||||
backgroundColor: '#f5f5f5',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
id: 'masonry-1-3',
|
||||
type: 'text',
|
||||
title: 'Tech Stack',
|
||||
content: 'Technologies: React, Node.js, MongoDB, Express, Stripe API',
|
||||
backgroundColor: '#e3f2fd',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'timeline-2',
|
||||
date: '2022',
|
||||
title: 'Frontend Developer',
|
||||
description: 'Designed and developed a responsive weather application providing real-time forecasts and historical weather data for locations worldwide.',
|
||||
actionUrl: 'https://github.com/username/weather-app',
|
||||
actionText: 'View Code',
|
||||
items: [
|
||||
{
|
||||
id: 'masonry-2-1',
|
||||
type: 'image',
|
||||
content: 'Weather app interface',
|
||||
imageUrl: 'https://source.unsplash.com/random/800x600?weather',
|
||||
width: 100,
|
||||
height: 180
|
||||
},
|
||||
{
|
||||
id: 'masonry-2-2',
|
||||
type: 'card',
|
||||
content: 'Created an intuitive UI with interactive maps and data visualizations for weather patterns.',
|
||||
backgroundColor: '#e8f5e9',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'timeline-3',
|
||||
date: '2021',
|
||||
title: 'Backend Developer',
|
||||
description: 'Developed a scalable task management system with features for task assignment, progress tracking, and automated notifications.',
|
||||
actionUrl: 'https://github.com/username/task-management',
|
||||
actionText: 'Explore Project',
|
||||
items: [
|
||||
{
|
||||
id: 'masonry-3-1',
|
||||
type: 'image',
|
||||
content: 'Task management dashboard',
|
||||
imageUrl: 'https://source.unsplash.com/random/800x600?tasks',
|
||||
width: 100,
|
||||
height: 200
|
||||
},
|
||||
{
|
||||
id: 'masonry-3-2',
|
||||
type: 'text',
|
||||
content: 'Implemented real-time notifications and collaborative features using WebSockets.',
|
||||
backgroundColor: '#fff3e0',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
id: 'masonry-3-3',
|
||||
type: 'card',
|
||||
content: 'Technologies: TypeScript, Node.js, MongoDB, Socket.io',
|
||||
backgroundColor: '#f5f5f5',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'timeline-4',
|
||||
date: '2020',
|
||||
title: 'UI/UX Designer',
|
||||
description: 'Created a personal portfolio website with modern design principles, responsive layouts, and interactive elements.',
|
||||
actionUrl: 'https://github.com/username/portfolio',
|
||||
actionText: 'View Portfolio',
|
||||
items: [
|
||||
{
|
||||
id: 'masonry-4-1',
|
||||
type: 'image',
|
||||
content: 'Portfolio homepage',
|
||||
imageUrl: 'https://source.unsplash.com/random/800x600?portfolio',
|
||||
width: 100,
|
||||
height: 180
|
||||
},
|
||||
{
|
||||
id: 'masonry-4-2',
|
||||
type: 'card',
|
||||
content: 'Designed with a focus on accessibility and performance optimization.',
|
||||
backgroundColor: '#e0f7fa',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'timeline-5',
|
||||
date: '2019',
|
||||
title: 'Mobile Developer',
|
||||
description: 'Developed a cross-platform fitness tracking application that helps users monitor workouts, set goals, and track progress.',
|
||||
actionUrl: 'https://github.com/username/fitness-tracker',
|
||||
actionText: 'See Project',
|
||||
items: [
|
||||
{
|
||||
id: 'masonry-5-1',
|
||||
type: 'image',
|
||||
content: 'Fitness tracker dashboard',
|
||||
imageUrl: 'https://source.unsplash.com/random/800x600?fitness',
|
||||
width: 100,
|
||||
height: 200
|
||||
},
|
||||
{
|
||||
id: 'masonry-5-2',
|
||||
type: 'text',
|
||||
content: 'Implemented data visualization for workout statistics and progress tracking.',
|
||||
backgroundColor: '#f3e5f5',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
id: 'masonry-5-3',
|
||||
type: 'card',
|
||||
content: 'Technologies: React Native, Firebase, Redux, Chart.js',
|
||||
backgroundColor: '#f5f5f5',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default timelineData;
|
Loading…
x
Reference in New Issue
Block a user