diff --git a/src/app/page.tsx b/src/app/page.tsx index f03cc71..688581f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 && } + d4m13n - .dev + + .dev + @@ -88,8 +102,25 @@ export default function HomePage() { } }} > - - + + Software + Software Dev Projects + + } /> + + Game Dev + Game Dev Projects + + } /> + @@ -106,7 +137,14 @@ export default function HomePage() { )} + {activeTab === 2 && ( + + + + )} + + {activeTab === 3 && ( About Me @@ -122,7 +160,7 @@ export default function HomePage() { )} - {activeTab === 3 && ( + {activeTab === 4 && ( Contact diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index c2f9c9e..c8bb753 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -223,13 +223,16 @@ const ProjectCard: React.FC = ({ 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 = ({ 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 = ({ 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', }} diff --git a/src/components/SocialIcons.tsx b/src/components/SocialIcons.tsx new file mode 100644 index 0000000..3a50c0f --- /dev/null +++ b/src/components/SocialIcons.tsx @@ -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) => ( + + + + + + + + + + + + + + + +); + +const DiscordIcon = (props: SvgIconProps) => ( + + + +); + +const XIcon = (props: SvgIconProps) => ( + + + +); + +const FilesIcon = (props: SvgIconProps) => ( + + + +); + +const AIIcon = (props: SvgIconProps) => ( + + + + +); + +// 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 = ({ + 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 ( + + {links.map((link) => ( + + + + + + ))} + + ); +}; + +export default SocialIcons; \ No newline at end of file diff --git a/src/components/Timeline/MasonryGrid.tsx b/src/components/Timeline/MasonryGrid.tsx new file mode 100644 index 0000000..4755fa3 --- /dev/null +++ b/src/components/Timeline/MasonryGrid.tsx @@ -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 = ({ + 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( + 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( + + ); + }); + + return columns; + }; + + const columns = getColumns(); + const columnCount = getColumnCount(); + + return ( + + {columns.map((column, index) => ( + + {column} + + ))} + + ); +}; + +export default MasonryGrid; \ No newline at end of file diff --git a/src/components/Timeline/MasonryItem.tsx b/src/components/Timeline/MasonryItem.tsx new file mode 100644 index 0000000..160ff89 --- /dev/null +++ b/src/components/Timeline/MasonryItem.tsx @@ -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 = ({ + 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 ( + + + + ); + + case 'card': + return ( + + + {item.content} + + {item.imageUrl && ( + + )} + + ); + + case 'text': + default: + return ( + + {item.content} + + ); + } + }; + + return ( + + + {renderContent()} + + + ); +}; + +export default MasonryItem; \ No newline at end of file diff --git a/src/components/Timeline/Timeline.tsx b/src/components/Timeline/Timeline.tsx new file mode 100644 index 0000000..ffb9838 --- /dev/null +++ b/src/components/Timeline/Timeline.tsx @@ -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 = ({ + items, + orientation = 'vertical', + className +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const timelineRef = useRef(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( + 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 ( + + {items.map((item, index) => ( + + + + ))} + + ); +}; + +export default Timeline; \ No newline at end of file diff --git a/src/components/Timeline/TimelineItem.tsx b/src/components/Timeline/TimelineItem.tsx new file mode 100644 index 0000000..ec0fa24 --- /dev/null +++ b/src/components/Timeline/TimelineItem.tsx @@ -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 = ({ + 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 = () => ( + + + + {item.date} + + + + {/* Line connecting to the next item */} + {effectiveOrientation === 'vertical' ? ( + + ) : ( + + )} + + ); + + // Render the content section + const renderContent = () => ( + + + + {item.title} + + + + {item.description} + + + + + {item.actionUrl && ( + + + + )} + + + ); + + // Layout for vertical orientation + if (effectiveOrientation === 'vertical') { + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* For alternating layout on desktop */} + {!isMobile && isAlternating ? ( + <> + {renderContent()} + + {renderMarker()} + + {/* Empty space for alignment */} + + ) : ( + <> + {/* Empty space for alignment */} + + {renderMarker()} + + {renderContent()} + + )} + + ); + } + + // Layout for horizontal orientation + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {isAlternating ? ( + <> + + {renderMarker()} + + {renderContent()} + + ) : ( + <> + {renderContent()} + + {renderMarker()} + + + )} + + ); +}; + +export default TimelineItem; \ No newline at end of file diff --git a/src/components/Timeline/index.ts b/src/components/Timeline/index.ts new file mode 100644 index 0000000..2a5a5d3 --- /dev/null +++ b/src/components/Timeline/index.ts @@ -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'; \ No newline at end of file diff --git a/src/components/Timeline/types.ts b/src/components/Timeline/types.ts new file mode 100644 index 0000000..fcad1e2 --- /dev/null +++ b/src/components/Timeline/types.ts @@ -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; +} \ No newline at end of file diff --git a/src/data/timeline.ts b/src/data/timeline.ts new file mode 100644 index 0000000..14bed03 --- /dev/null +++ b/src/data/timeline.ts @@ -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; \ No newline at end of file