feat: resume

This commit is contained in:
Damien 2025-02-25 10:07:23 -05:00
parent a60f3cf0b5
commit 139bc201b8
10 changed files with 995 additions and 12 deletions

View File

@ -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

View File

@ -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',
}}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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';

View 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
View 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;