feat: project pages

This commit is contained in:
Damien 2025-02-25 09:36:36 -05:00
parent c806a772e3
commit a60f3cf0b5
6 changed files with 707 additions and 14 deletions

View File

@ -17,7 +17,7 @@ import UsersIcon from '@mui/icons-material/People'
import ThemeRegistry from '@/components/ThemeRegistry/ThemeRegistry' import ThemeRegistry from '@/components/ThemeRegistry/ThemeRegistry'
export const metadata = { export const metadata = {
title: 'Next.js MUI Starter Template', title: 'd4m13n.dev',
description: 'Next.js App Router + Material UI v5 Starter Template' description: 'Next.js App Router + Material UI v5 Starter Template'
} }

View File

@ -5,16 +5,26 @@ import { useState } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Fade from '@mui/material/Fade'; import Fade from '@mui/material/Fade';
import Button from '@mui/material/Button';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import TypingAnimation from '@/components/TypingAnimation'; import TypingAnimation from '@/components/TypingAnimation';
import ProjectMasonry from '@/components/ProjectMasonry';
import projects from '@/data/projects';
export default function HomePage() { export default function HomePage() {
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
return ( return (
<> <>
{!showContent && ( {!showContent && (
<TypingAnimation <TypingAnimation
titleText="My name is Damien." titleText="My name is D4m13n."
subtitleText="Welcome to my website!" subtitleText="Welcome to my website!"
typingSpeed={80} typingSpeed={80}
delayBeforeRemoval={3000} delayBeforeRemoval={3000}
@ -27,18 +37,117 @@ export default function HomePage() {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', width: '100%',
minHeight: '100vh', p: { xs: 2, sm: 3, md: 4 },
p: 4,
visibility: showContent ? 'visible' : 'hidden' visibility: showContent ? 'visible' : 'hidden'
}}> }}>
<Typography variant="h3" component="h1" gutterBottom> <Typography
Damien's Website variant="h3"
component="h1"
gutterBottom
sx={{
textAlign: 'center',
fontWeight: 'bold',
mb: 4
}}
>
<Box
component="span"
sx={{
color: '#4fd1ff',
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))'
}}
>
d4m13n
</Box>
<Box component="span">.dev</Box>
</Typography> </Typography>
<Typography variant="body1" paragraph>
This is a test sentence. <Box sx={{ width: '100%', mb: 4 }}>
</Typography> <Tabs
{/* Add more content here */} value={activeTab}
onChange={handleTabChange}
centered
sx={{
'& .MuiTabs-indicator': {
backgroundColor: '#4fd1ff',
height: 3,
boxShadow: '0px 0px 15px rgba(79, 209, 255, 0.3)',
},
'& .MuiTab-root': {
transition: 'all 0.3s ease-in-out',
'&:hover': {
color: '#4fd1ff',
textShadow: '0px 0px 15px rgba(79, 209, 255, 0.3)',
},
},
'& .Mui-selected': {
color: '#4fd1ff !important',
textShadow: '0px 0px 15px rgba(79, 209, 255, 0.3)',
}
}}
>
<Tab label="Software Dev Projects" />
<Tab label="Game Dev Projects" />
<Tab label="About" />
<Tab label="Contact" />
</Tabs>
</Box>
{activeTab === 0 && (
<Box sx={{ width: '100%', maxWidth: 1200, mx: 'auto' }}>
<ProjectMasonry projects={projects} />
</Box>
)}
{activeTab === 1 && (
<Box sx={{ width: '100%', maxWidth: 1200, mx: 'auto', p: 2 }}>
<ProjectMasonry projects={projects} />
</Box>
)}
{activeTab === 2 && (
<Box sx={{ width: '100%', maxWidth: 800, mx: 'auto', p: 2 }}>
<Typography variant="h5" component="h2" gutterBottom sx={{ mb: 2 }}>
About Me
</Typography>
<Typography variant="body1" paragraph>
I'm a passionate developer with expertise in modern web technologies.
I love creating responsive, user-friendly applications that solve real-world problems.
</Typography>
<Typography variant="body1">
When I'm not coding, you can find me exploring new technologies, contributing to open-source projects,
or enjoying outdoor activities.
</Typography>
</Box>
)}
{activeTab === 3 && (
<Box sx={{ width: '100%', maxWidth: 800, mx: 'auto', p: 2 }}>
<Typography variant="h5" component="h2" gutterBottom sx={{ mb: 2 }}>
Contact
</Typography>
<Typography variant="body1" paragraph>
Feel free to reach out to me for collaboration opportunities or just to say hello!
</Typography>
<Button
variant="contained"
href="mailto:contact@example.com"
sx={{
backgroundColor: '#1a365d',
color: '#ffffff',
boxShadow: '0px 4px 8px rgba(0,0,0,0.3)',
'&:hover': {
backgroundColor: '#2a466d',
boxShadow: '0px 0px 15px rgba(79, 209, 255, 0.3)',
},
transition: 'all 0.3s ease-in-out',
}}
>
Email Me
</Button>
</Box>
)}
</Box> </Box>
</Fade> </Fade>
</> </>

View File

@ -0,0 +1,313 @@
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
CardMedia,
Typography,
Button,
IconButton,
Chip,
Stack,
useTheme,
useMediaQuery
} from '@mui/material';
import GitHubIcon from '@mui/icons-material/GitHub';
import LaunchIcon from '@mui/icons-material/Launch';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
// Define the project image interface
interface ProjectImage {
src: string;
alt: string;
}
// Define the repository interface
interface Repository {
name: string;
url: string;
}
// Define the project interface
export interface Project {
id: string;
title: string;
description: string;
technologies: string[];
images: ProjectImage[];
repositories?: Repository[]; // Multiple repositories with names
demoUrl?: string;
}
interface ProjectCardProps {
project: Project;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// Colors and effects based on specifications
const cardBgColor = theme.palette.grey[900]; // Dark grey from theme
const textColor = '#ffffff'; // Crisp white
const dropShadow = '0px 4px 8px rgba(0,0,0,0.3)';
const ambientGlow = '0px 0px 15px rgba(255, 255, 255, 0.15)'; // White glow
const enhancedGlow = '0px 0px 20px rgba(255, 255, 255, 0.3)'; // Enhanced white glow
const titleColor = '#4fd1ff'; // Same blue as active tab
const titleGlow = '0px 0px 10px rgba(255, 255, 255, 0.15), 0px 0px 20px rgba(255, 255, 255, 0.1)'; // White glow for title
// Handle image navigation
const handlePrevImage = (e: React.MouseEvent) => {
e.stopPropagation();
setCurrentImageIndex(prev =>
prev === 0 ? project.images.length - 1 : prev - 1
);
};
const handleNextImage = (e: React.MouseEvent) => {
e.stopPropagation();
setCurrentImageIndex(prev =>
prev === project.images.length - 1 ? 0 : prev + 1
);
};
return (
<Card
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
sx={{
backgroundColor: cardBgColor, // Use the deep blue background color
color: textColor,
boxShadow: isHovered ? enhancedGlow : dropShadow,
transition: 'all 0.3s ease-in-out',
height: '100%', // Ensure card takes full height
display: 'flex',
flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: enhancedGlow,
},
}}
>
{project.images.length > 0 && (
<Box sx={{ position: 'relative' }}>
<CardMedia
component="img"
image={project.images[currentImageIndex].src}
alt={project.images[currentImageIndex].alt}
sx={{
objectFit: 'cover',
height: { xs: '180px', sm: '220px', md: '240px' }, // Responsive height
loading: 'lazy', // Enable lazy loading for performance
transition: 'all 0.3s ease-in-out'
}}
/>
{/* Image navigation controls - only show if there are multiple images */}
{project.images.length > 1 && (
<>
<IconButton
size="small"
onClick={handlePrevImage}
sx={{
position: 'absolute',
left: 8,
top: '50%',
transform: 'translateY(-50%)',
backgroundColor: 'rgba(0,0,0,0.5)',
color: textColor,
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.7)',
boxShadow: ambientGlow,
},
transition: 'all 0.3s ease-in-out',
opacity: isHovered || isMobile ? 1 : 0,
}}
>
<ArrowBackIosNewIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={handleNextImage}
sx={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
backgroundColor: 'rgba(0,0,0,0.5)',
color: textColor,
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.7)',
boxShadow: ambientGlow,
},
transition: 'all 0.3s ease-in-out',
opacity: isHovered || isMobile ? 1 : 0,
}}
>
<ArrowForwardIosIcon fontSize="small" />
</IconButton>
{/* Image counter indicator */}
<Box
sx={{
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(0,0,0,0.5)',
color: textColor,
padding: '2px 8px',
borderRadius: '10px',
fontSize: '0.75rem',
opacity: isHovered || isMobile ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
>
{currentImageIndex + 1} / {project.images.length}
</Box>
</>
)}
</Box>
)}
<CardContent sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
p: { xs: 2, sm: 3 }, // Responsive padding
'&:last-child': { pb: { xs: 2, sm: 3 } } // Override MUI's default padding-bottom
}}>
<Typography
variant="h5"
component="div"
sx={{
fontWeight: 'bold',
color: titleColor,
textShadow: titleGlow,
mb: 1.5,
lineHeight: 1.2
}}
>
{project.title}
</Typography>
<Typography
variant="body2"
sx={{
mb: 2.5,
flexGrow: 1,
opacity: 0.9,
lineHeight: 1.6,
letterSpacing: '0.015em'
}}
>
{project.description}
</Typography>
<Box
sx={{
mb: 3,
display: 'flex',
flexWrap: 'wrap',
gap: 0.75
}}
>
{project.technologies.map((tech, index) => (
<Chip
key={index}
label={tech}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
color: textColor,
height: 24,
fontSize: '0.75rem',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.2)',
boxShadow: ambientGlow,
},
transition: 'all 0.3s ease-in-out',
}}
/>
))}
</Box>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1.5,
justifyContent: 'flex-end',
mt: 'auto', // Push buttons to bottom of card
pt: 1 // Add some padding at the top
}}
>
{/* Repository buttons */}
{project.repositories && project.repositories.length > 0 && (
<>
{project.repositories.map((repo, index) => (
<Button
key={index}
variant="outlined"
size="small"
startIcon={<GitHubIcon />}
href={repo.url}
target="_blank"
rel="noopener noreferrer"
sx={{
color: textColor,
borderColor: 'rgba(255,255,255,0.3)',
borderRadius: '6px',
padding: '4px 12px',
minWidth: '80px',
'&:hover': {
borderColor: textColor,
backgroundColor: 'rgba(255,255,255,0.05)',
boxShadow: ambientGlow,
},
transition: 'all 0.3s ease-in-out',
}}
>
{repo.name}
</Button>
))}
</>
)}
{/* Demo button */}
{project.demoUrl && (
<Button
variant="contained"
size="small"
startIcon={<LaunchIcon />}
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
sx={{
backgroundColor: 'rgba(79, 209, 255, 0.4)',
color: textColor,
borderRadius: '6px',
padding: '4px 12px',
minWidth: '80px',
'&:hover': {
backgroundColor: 'rgba(79, 209, 255, 0.4)',
boxShadow: enhancedGlow,
},
transition: 'all 0.3s ease-in-out',
}}
>
Demo
</Button>
)}
</Box>
</CardContent>
</Card>
);
};
export default ProjectCard;

View File

@ -0,0 +1,90 @@
import React, { useEffect, useState } from 'react';
import { Box, Grid, useMediaQuery, useTheme } from '@mui/material';
import ProjectCard, { Project } from './ProjectCard';
interface ProjectMasonryProps {
projects: Project[];
}
const ProjectMasonry: React.FC<ProjectMasonryProps> = ({ projects }) => {
const theme = useTheme();
const isXs = useMediaQuery(theme.breakpoints.only('xs'));
const isSm = useMediaQuery(theme.breakpoints.only('sm'));
const isMd = useMediaQuery(theme.breakpoints.only('md'));
// Assign varying widths to projects
const getProjectWidths = () => {
return projects.map((project, index) => {
// Create a pattern of varying widths
// This creates a more interesting masonry layout
if (isXs) return 12; // On mobile, all cards are full width
// Create a pattern for varying widths
const patterns = [
// Pattern 1: [6, 6, 12, 6, 6]
[6, 6, 12, 6, 6],
// Pattern 2: [8, 4, 4, 8, 12]
[8, 4, 4, 8, 12],
// Pattern 3: [4, 8, 4, 8, 4, 8]
[4, 8, 4, 8, 4, 8]
];
// Select a pattern based on the index
const patternIndex = Math.floor(index / 5) % patterns.length;
const pattern = patterns[patternIndex];
const positionInPattern = index % pattern.length;
return pattern[positionInPattern];
});
};
const projectWidths = getProjectWidths();
return (
<Box
sx={{
width: '100%',
position: 'relative',
px: { xs: 1, sm: 2 }, // Add horizontal padding that scales with screen size
}}
>
<Grid container spacing={3} alignItems="flex-start">
{projects.map((project, index) => (
<Grid
item
xs={12}
sm={projectWidths[index]}
key={project.id}
sx={{
mb: 3,
opacity: 1,
transform: 'translateY(0)',
transition: 'all 0.4s ease-in-out',
'&:hover': {
zIndex: 1,
},
// Add animation for when items are added to the DOM
'@keyframes fadeIn': {
from: {
opacity: 0,
transform: 'translateY(20px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
},
animation: 'fadeIn 0.5s ease-in-out',
// Stagger the animation based on index
animationDelay: `${index * 0.05}s`,
}}
>
<ProjectCard project={project} />
</Grid>
))}
</Grid>
</Box>
);
};
export default ProjectMasonry;

View File

@ -116,7 +116,7 @@ const TypingAnimation: React.FC<TypingAnimationProps> = ({
// Blue glow for "Damien" // Blue glow for "Damien"
const blueGlowEffects = { const blueGlowEffects = {
textShadow: '0 0 10px rgba(25, 118, 210, 0.35), 0 0 20px rgba(25, 118, 210, 0.25)', 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))' filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))'
}; };
@ -132,7 +132,7 @@ const TypingAnimation: React.FC<TypingAnimationProps> = ({
const fullTitleText = titleText; const fullTitleText = titleText;
// Find where "Damien" starts in the full text // Find where "Damien" starts in the full text
const damienStartIndex = fullTitleText.toLowerCase().indexOf('damien'); const damienStartIndex = fullTitleText.toLowerCase().indexOf('d4m13n');
// If "Damien" isn't in the text (shouldn't happen, but just in case) // If "Damien" isn't in the text (shouldn't happen, but just in case)
if (damienStartIndex === -1) { if (damienStartIndex === -1) {
@ -159,7 +159,7 @@ const TypingAnimation: React.FC<TypingAnimationProps> = ({
key={i} key={i}
component="span" component="span"
sx={{ sx={{
color: '#1976d2', /* Material UI primary blue */ color: '#4fd1ff', /* Same blue as active tab */
...blueGlowEffects ...blueGlowEffects
}} }}
> >

181
src/data/projects.ts Normal file
View File

@ -0,0 +1,181 @@
import { Project } from '@/components/ProjectCard';
// Sample project data
const projects: Project[] = [
{
id: '1',
title: 'E-Commerce Platform',
description: 'A full-featured e-commerce platform with product management, shopping cart, and payment processing capabilities.',
technologies: ['React', 'Node.js', 'MongoDB', 'Stripe'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?ecommerce',
alt: 'E-commerce dashboard'
},
{
src: 'https://source.unsplash.com/random/800x600?shopping',
alt: 'Shopping cart interface'
},
{
src: 'https://source.unsplash.com/random/800x600?payment',
alt: 'Payment processing screen'
}
],
repositories: [
{ name: 'Frontend', url: 'https://github.com/username/ecommerce-frontend' },
{ name: 'Backend', url: 'https://github.com/username/ecommerce-api' }
],
demoUrl: 'https://ecommerce-demo.example.com'
},
{
id: '2',
title: 'Weather App',
description: 'A responsive weather application that provides real-time weather data and forecasts for locations worldwide.',
technologies: ['JavaScript', 'React', 'OpenWeather API', 'CSS'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?weather',
alt: 'Weather app interface'
},
{
src: 'https://source.unsplash.com/random/800x600?forecast',
alt: 'Forecast view'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/weather-app' }
],
demoUrl: 'https://weather-app-demo.example.com'
},
{
id: '3',
title: 'Task Management System',
description: 'A comprehensive task management system with features like task assignment, progress tracking, and deadline notifications.',
technologies: ['TypeScript', 'Angular', 'Firebase', 'Material UI'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?tasks',
alt: 'Task management dashboard'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/task-management' }
],
demoUrl: 'https://task-app-demo.example.com'
},
{
id: '4',
title: 'Portfolio Website',
description: 'A personal portfolio website showcasing projects, skills, and professional experience with a modern, responsive design.',
technologies: ['HTML', 'CSS', 'JavaScript', 'GSAP'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?portfolio',
alt: 'Portfolio homepage'
},
{
src: 'https://source.unsplash.com/random/800x600?website',
alt: 'Projects section'
},
{
src: 'https://source.unsplash.com/random/800x600?design',
alt: 'Contact form'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/portfolio' }
],
demoUrl: 'https://portfolio-demo.example.com'
},
{
id: '5',
title: 'Recipe Finder',
description: 'An application that allows users to search for recipes based on ingredients, dietary restrictions, and cuisine preferences.',
technologies: ['React', 'Redux', 'Spoonacular API', 'Styled Components'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?recipe',
alt: 'Recipe search interface'
},
{
src: 'https://source.unsplash.com/random/800x600?food',
alt: 'Recipe details'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/recipe-finder' }
],
demoUrl: 'https://recipe-finder-demo.example.com'
},
{
id: '6',
title: 'Fitness Tracker',
description: 'A fitness tracking application that helps users monitor workouts, set goals, and track progress over time.',
technologies: ['React Native', 'Firebase', 'Redux', 'Chart.js'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?fitness',
alt: 'Fitness tracker dashboard'
},
{
src: 'https://source.unsplash.com/random/800x600?workout',
alt: 'Workout tracking screen'
},
{
src: 'https://source.unsplash.com/random/800x600?exercise',
alt: 'Progress charts'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/fitness-tracker' }
],
demoUrl: 'https://fitness-app-demo.example.com'
},
{
id: '7',
title: 'Chat Application',
description: 'A real-time chat application with features like private messaging, group chats, and file sharing capabilities.',
technologies: ['Socket.io', 'Express', 'MongoDB', 'React'],
images: [
{
src: 'https://source.unsplash.com/random/800x600?chat',
alt: 'Chat interface'
},
{
src: 'https://source.unsplash.com/random/800x600?messaging',
alt: 'Messaging screen'
}
],
repositories: [
{ name: 'Repo', url: 'https://github.com/username/chat-app' }
],
demoUrl: 'https://chat-app-demo.example.com'
},
// {
// id: '8',
// title: 'Budget Tracker',
// description: 'A financial management application that helps users track income, expenses, and savings goals with visual reports.',
// technologies: ['Vue.js', 'Node.js', 'PostgreSQL', 'D3.js'],
// images: [
// {
// src: 'https://source.unsplash.com/random/800x600?budget',
// alt: 'Budget dashboard'
// },
// {
// src: 'https://source.unsplash.com/random/800x600?finance',
// alt: 'Expense tracking'
// },
// {
// src: 'https://source.unsplash.com/random/800x600?money',
// alt: 'Financial reports'
// }
// ],
// repositories: [
// { name: 'Frontend', url: 'https://github.com/username/budget-tracker-ui' },
// { name: 'API', url: 'https://github.com/username/budget-tracker-api' }
// ],
// demoUrl: 'https://budget-app-demo.example.com'
// }
];
export default projects;