add: portfolio project

This commit is contained in:
ysahih 2025-04-11 02:05:41 +01:00
parent fbdd4e3e29
commit 97ae8dda83
27 changed files with 11994 additions and 1 deletions

5
.gitignore vendored
View File

@ -53,6 +53,9 @@ cmake-build-*/
# IntelliJ
out/
node_modules
.next
.env
# mpeltonen/sbt-idea plugin
.idea_modules/
@ -169,4 +172,4 @@ tags
# End of https://www.gitignore.io/api/linux,visualstudiocode,jetbrains+all,sublimetext,vim
./src/alphabetical.py
./src/alphabetical.py

View File

@ -0,0 +1,58 @@
"use client";
import React, { createContext, useState, useContext, useEffect } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>('dark');
// Initialize theme from localStorage if available (client-side only)
useEffect(() => {
const storedTheme = localStorage.getItem('theme') as Theme | null;
if (storedTheme) {
setTheme(storedTheme);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark');
}
}, []);
// Update document when theme changes
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
export default ThemeContext;

View File

@ -0,0 +1,146 @@
"use client";
import { motion } from 'framer-motion';
import { CodeIcon, BookOpenIcon, BrainCircuitIcon, GraduationCapIcon } from 'lucide-react';
interface AboutProps {
data: {
summary: string;
name: string;
};
}
const AboutSection: React.FC<AboutProps> = ({ data }) => {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100
}
}
};
const qualities = [
{
icon: <CodeIcon className="w-6 h-6" />,
title: "Problem Solver",
description: "Approaching complex coding challenges with analytical thinking and persistence."
},
{
icon: <BookOpenIcon className="w-6 h-6" />,
title: "Continuous Learner",
description: "Passionate about staying updated with the latest technologies and best practices."
},
{
icon: <BrainCircuitIcon className="w-6 h-6" />,
title: "Creative Thinker",
description: "Finding innovative solutions by thinking outside the box."
},
{
icon: <GraduationCapIcon className="w-6 h-6" />,
title: "Academic Focus",
description: "Pursuing a Master's Degree in IT Architecture in the 42 network."
}
];
return (
<div className="py-20 px-4 sm:px-6 md:px-12 relative overflow-hidden" id="about">
{/* Background shape */}
<div className="absolute right-0 top-0 w-96 h-96 bg-primary/5 dark:bg-primary/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl -z-10" />
<div className="absolute left-0 bottom-0 w-96 h-96 bg-secondary/5 dark:bg-secondary/10 rounded-full translate-y-1/2 -translate-x-1/2 blur-3xl -z-10" />
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">About Me</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
</motion.div>
<div className="flex flex-col lg:flex-row gap-12 items-center">
{/* Left side - Image */}
<motion.div
className="w-full lg:w-5/12"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-br from-primary to-secondary opacity-20 rounded-lg transform rotate-3 scale-105" />
<div className="absolute inset-0 bg-white dark:bg-gray-800 rounded-lg transform -rotate-3 scale-105" />
<div className="relative bg-surface-light dark:bg-surface-dark p-6 rounded-lg shadow-xl">
<h3 className="text-2xl font-semibold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">Who I Am</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-4">
{data.summary}
</p>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="font-medium text-gray-700 dark:text-gray-200">
Currently pursuing my passion for programming in the 42 network, working toward a Master's Degree in IT Architecture.
</p>
</div>
</div>
</div>
</motion.div>
{/* Right side - Qualities */}
<motion.div
className="w-full lg:w-7/12"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
<h3 className="text-2xl font-semibold mb-6 text-center lg:text-left">My Qualities</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{qualities.map((quality, index) => (
<motion.div
key={index}
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 group"
variants={itemVariants}
whileHover={{ y: -5 }}
>
<div className="mb-4 inline-flex p-3 bg-primary/10 dark:bg-primary/20 rounded-lg text-primary dark:text-primary-light group-hover:bg-primary/20 dark:group-hover:bg-primary/30 transition-colors duration-300">
{quality.icon}
</div>
<h4 className="text-xl font-semibold mb-2">{quality.title}</h4>
<p className="text-gray-600 dark:text-gray-300">
{quality.description}
</p>
</motion.div>
))}
</div>
<motion.div
className="mt-8 text-center lg:text-left"
variants={itemVariants}
>
<p className="text-lg text-gray-600 dark:text-gray-300">
I am <span className="font-semibold text-primary dark:text-primary-light">passionate about solving problems</span> through code and building applications that provide genuine value.
</p>
</motion.div>
</motion.div>
</div>
</div>
</div>
);
};
export default AboutSection;

View File

@ -0,0 +1,373 @@
"use client";
import { useState, FormEvent, useEffect } from 'react';
import { motion } from 'framer-motion';
import { MailIcon, PhoneIcon, MapPinIcon, SendIcon, CheckCircleIcon, AlertCircleIcon } from 'lucide-react';
import { toast } from 'react-hot-toast';
import emailjs from 'emailjs-com';
interface ContactProps {
contact: {
email: string;
tel: string;
social: {
name: string;
url: string;
icon: any;
}[];
};
location: string;
}
type FormData = {
name: string;
email: string;
subject: string;
message: string;
};
type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
const ContactSection: React.FC<ContactProps> = ({ contact, location }) => {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [formStatus, setFormStatus] = useState<FormStatus>('idle');
const [errors, setErrors] = useState<Partial<FormData>>({});
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user types
if (errors[name as keyof FormData]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};
useEffect(() => {
emailjs.init('k7Xbdqofx3sBAJf3I');
}, []);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) {
toast.error('Please fix the errors in the form');
return;
}
setFormStatus('submitting');
try {
await emailjs.send(
'service_s5tsk8o',
'template_9rbucml',
{
from_name: formData.name,
from_email: formData.email,
subject: formData.subject,
message: formData.message,
},
'user_id'
);
setFormStatus('success');
toast.success('Message sent successfully!');
setFormData({ name: '', email: '', subject: '', message: '' });
// Reset to idle after 3 seconds
setTimeout(() => {
setFormStatus('idle');
}, 3000);
} catch (error) {
setFormStatus('error');
toast.error('Failed to send message. Please try again.');
console.error('Email error:', error);
// Reset to idle after 3 seconds
setTimeout(() => {
setFormStatus('idle');
}, 3000);
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
},
},
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 overflow-hidden" id="contact">
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Get In Touch</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
I'm always open to discussing new projects, creative ideas, or opportunities to be part of your vision.
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Contact Info */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
<h3 className="text-2xl font-semibold mb-8">Contact Information</h3>
<div className="space-y-6">
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<MailIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Email</h4>
<a href={`mailto:${contact.email}`} className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors">
{contact.email}
</a>
</div>
</motion.div>
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<PhoneIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Phone</h4>
<a href={`tel:${contact.tel}`} className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors">
{contact.tel}
</a>
</div>
</motion.div>
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<MapPinIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Location</h4>
<p className="text-gray-600 dark:text-gray-300">{location}</p>
</div>
</motion.div>
</div>
<motion.div
className="mt-12"
variants={itemVariants}
>
<h4 className="font-medium text-lg mb-3">Connect with me</h4>
<div className="flex space-x-4">
{contact.social.map((platform, index) => (
<motion.a
key={index}
href={platform.url}
target="_blank"
rel="noopener noreferrer"
className="bg-white dark:bg-gray-800 p-3 rounded-lg shadow-md text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:shadow-lg transition-all duration-300"
whileHover={{ y: -5 }}
whileTap={{ scale: 0.95 }}
>
<platform.icon className="w-6 h-6" />
</motion.a>
))}
</div>
</motion.div>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-8">
<h3 className="text-2xl font-semibold mb-6">Send Me a Message</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="Your name"
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="your.email@example.com"
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Subject
</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.subject ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="What is this regarding?"
/>
{errors.subject && <p className="mt-1 text-sm text-red-500">{errors.subject}</p>}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Message
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.message ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="Your message here..."
/>
{errors.message && <p className="mt-1 text-sm text-red-500">{errors.message}</p>}
</div>
<div>
<motion.button
type="submit"
className={`w-full py-3 px-6 rounded-lg text-white font-medium flex items-center justify-center transition-all duration-300 ${
formStatus === 'submitting'
? 'bg-gray-400 cursor-not-allowed'
: formStatus === 'success'
? 'bg-green-500 hover:bg-green-600'
: formStatus === 'error'
? 'bg-red-500 hover:bg-red-600'
: 'bg-primary hover:bg-primary-dark'
}`}
disabled={formStatus === 'submitting'}
whileHover={formStatus !== 'submitting' ? { scale: 1.02 } : {}}
whileTap={formStatus !== 'submitting' ? { scale: 0.98 } : {}}
>
{formStatus === 'submitting' ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
) : formStatus === 'success' ? (
<span className="flex items-center">
<CheckCircleIcon className="w-5 h-5 mr-2" />
Message Sent!
</span>
) : formStatus === 'error' ? (
<span className="flex items-center">
<AlertCircleIcon className="w-5 h-5 mr-2" />
Failed to Send
</span>
) : (
<span className="flex items-center">
<SendIcon className="w-5 h-5 mr-2" />
Send Message
</span>
)}
</motion.button>
</div>
</form>
</div>
</motion.div>
</div>
</div>
</div>
);
};
export default ContactSection;

View File

@ -0,0 +1,133 @@
"use client";
import { motion } from 'framer-motion';
import { GraduationCapIcon, CalendarIcon, BookOpenIcon } from 'lucide-react';
interface Education {
school: string;
degree: string;
start: string;
end: string;
}
interface EducationSectionProps {
education: Education[];
}
const EducationSection: React.FC<EducationSectionProps> = ({ education }) => {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { x: -50, opacity: 0 },
visible: {
x: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
damping: 12,
},
},
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 bg-surface-light dark:bg-surface-dark overflow-hidden" id="education">
<div className="container mx-auto max-w-6xl relative">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Education Journey</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
My academic background and educational qualifications that have shaped my skills and knowledge.
</p>
</motion.div>
{/* Education Timeline */}
<div className="relative">
{/* Timeline line */}
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-1 bg-gradient-to-b from-primary to-secondary rounded-full transform -translate-x-1/2 md:translate-x-0"></div>
<motion.div
className="space-y-12"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
{education.map((edu, index) => (
<motion.div
key={index}
className={`relative flex items-center ${
index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'
}`}
variants={itemVariants}
>
{/* Timeline point */}
<div className="absolute left-4 md:left-1/2 w-8 h-8 bg-white dark:bg-gray-800 rounded-full border-4 border-primary dark:border-primary-light transform -translate-x-1/2 flex items-center justify-center z-10">
<GraduationCapIcon className="w-4 h-4 text-primary dark:text-primary-light" />
</div>
{/* Content */}
<div className={`w-full md:w-5/12 ${
index % 2 === 0 ? 'md:pr-16 md:text-right' : 'md:pl-16'
}`}>
<motion.div
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg"
whileHover={{ y: -5, boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)" }}
>
<h3 className="text-xl font-bold text-gray-800 dark:text-white mb-1">{edu.school}</h3>
<p className="text-primary dark:text-primary-light font-medium mb-3">{edu.degree}</p>
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-4 justify-start md:justify-end">
<CalendarIcon className="w-4 h-4" />
<span>{edu.start} - {edu.end}</span>
</div>
{index === 1 && (
<div className="mt-4 bg-primary/10 dark:bg-primary/20 p-3 rounded-lg">
<div className="flex items-start gap-2">
<BookOpenIcon className="w-5 h-5 text-primary dark:text-primary-light mt-1" />
<p className="text-sm text-gray-700 dark:text-gray-300">
Part of the prestigious 42 Network, known for its project-based, peer-to-peer learning methodology.
</p>
</div>
</div>
)}
</motion.div>
</div>
</motion.div>
))}
</motion.div>
</div>
{/* Education Quote */}
<motion.div
className="mt-16 text-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<blockquote className="text-xl italic text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
"Education is the passport to the future, for tomorrow belongs to those who prepare for it today."
</blockquote>
<p className="mt-2 text-gray-600 dark:text-gray-400"> Malcolm X</p>
</motion.div>
</div>
</div>
);
};
export default EducationSection;

View File

@ -0,0 +1,196 @@
"use client";
import { motion } from 'framer-motion';
import { BriefcaseIcon, CalendarIcon, MapPinIcon, CheckIcon } from 'lucide-react';
interface Experience {
company: string;
position: string;
logo?: string;
location: string;
startDate: string;
endDate: string;
description: string;
skills: readonly string[];
achievements: readonly string[];
}
interface ExperienceSectionProps {
experience: readonly Experience[];
}
const ExperienceSection: React.FC<ExperienceSectionProps> = ({ experience }) => {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { x: -50, opacity: 0 },
visible: {
x: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
damping: 12,
},
},
};
// Function to generate a default logo if none provided
const generateDefaultLogo = (companyName: string) => {
return `https://ui-avatars.com/api/?name=${encodeURIComponent(companyName)}&background=0D8ABC&color=fff&size=128`;
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 bg-surface-light dark:bg-surface-dark overflow-hidden" id="experience">
<div className="container mx-auto max-w-6xl relative">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Work Experience</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
My professional journey and key accomplishments in the tech industry.
</p>
</motion.div>
{/* Experience Timeline */}
<div className="relative">
{/* Timeline line */}
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-1 bg-gradient-to-b from-primary to-secondary rounded-full transform -translate-x-1/2 md:translate-x-0"></div>
<motion.div
className="space-y-12"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
{experience.map((exp, index) => (
<motion.div
key={index}
className={`relative flex items-center ${
index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'
}`}
variants={itemVariants}
>
{/* Timeline point */}
<div className="absolute left-4 md:left-1/2 w-8 h-8 bg-white dark:bg-gray-800 rounded-full border-4 border-primary dark:border-primary-light transform -translate-x-1/2 flex items-center justify-center z-10">
<BriefcaseIcon className="w-4 h-4 text-primary dark:text-primary-light" />
</div>
{/* Content */}
<div className={`w-full md:w-5/12 ${
index % 2 === 0 ? 'md:pr-16 md:text-right' : 'md:pl-16'
}`}>
<motion.div
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg"
whileHover={{ y: -5, boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)" }}
>
<div className="flex items-center gap-4 mb-4">
<div className={`w-12 h-12 rounded-full overflow-hidden ${index % 2 === 1 ? 'order-1' : 'md:order-2'}`}>
<img
src={exp.logo || generateDefaultLogo(exp.company)}
alt={`${exp.company} logo`}
className="w-full h-full object-cover"
/>
</div>
<div className={index % 2 === 1 ? 'order-2' : 'md:order-1 md:text-right'}>
<h3 className="text-xl font-bold text-gray-800 dark:text-white">{exp.position}</h3>
<p className="text-primary dark:text-primary-light font-medium">{exp.company}</p>
</div>
</div>
<div className="space-y-3">
<div className={`flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 ${
index % 2 === 0 ? 'md:justify-end' : 'justify-start'
}`}>
<CalendarIcon className="w-4 h-4 flex-shrink-0" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<div className={`flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 ${
index % 2 === 0 ? 'md:justify-end' : 'justify-start'
}`}>
<MapPinIcon className="w-4 h-4 flex-shrink-0" />
<span>{exp.location}</span>
</div>
<p className="text-gray-600 dark:text-gray-300 py-3 border-t border-b border-gray-200 dark:border-gray-700 my-3">
{exp.description}
</p>
{/* Skills used */}
<div className={`flex flex-wrap gap-2 mb-4 ${
index % 2 === 0 ? 'md:justify-end' : 'justify-start'
}`}>
{exp.skills.map((skill, skillIndex) => (
<span
key={skillIndex}
className="px-2 py-1 text-xs bg-primary/10 dark:bg-primary/20 text-primary dark:text-primary-light rounded-full"
>
{skill}
</span>
))}
</div>
{/* Achievements */}
<div className="space-y-2">
<h4 className={`text-sm font-semibold ${
index % 2 === 0 ? 'md:text-right' : 'text-left'
}`}>
Key Achievements:
</h4>
<ul className={`space-y-1 ${
index % 2 === 0 ? 'md:text-right' : 'text-left'
}`}>
{exp.achievements.map((achievement, achIndex) => (
<li key={achIndex} className="flex items-start gap-2">
<CheckIcon className={`w-4 h-4 text-green-500 mt-1 flex-shrink-0 ${
index % 2 === 0 ? 'md:order-2' : 'order-1'
}`} />
<span className="text-sm text-gray-600 dark:text-gray-300">
{achievement}
</span>
</li>
))}
</ul>
</div>
</div>
</motion.div>
</div>
</motion.div>
))}
</motion.div>
</div>
{/* Quote at the bottom */}
<motion.div
className="mt-16 text-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<blockquote className="text-xl italic text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
"Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful."
</blockquote>
<p className="mt-2 text-gray-600 dark:text-gray-400"> Albert Schweitzer</p>
</motion.div>
</div>
</div>
);
};
export default ExperienceSection;

173
app/components/Footer.tsx Normal file
View File

@ -0,0 +1,173 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { ArrowUpIcon } from "lucide-react";
interface FooterProps {
socialLinks: {
name: string;
url: string;
icon: any;
}[];
}
const Footer: React.FC<FooterProps> = ({ socialLinks }) => {
const [showScrollTop, setShowScrollTop] = useState(false);
useEffect(() => {
const handleScroll = () => {
setShowScrollTop(window.scrollY > 500);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
const navItems = [
{ id: "home", label: "Home" },
{ id: "about", label: "About" },
{ id: "skills", label: "Skills" },
{ id: "projects", label: "Projects" },
{ id: "education", label: "Education" },
{ id: "contact", label: "Contact" },
];
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
window.scrollTo({
top: element.offsetTop - 80,
behavior: "smooth",
});
}
};
return (
<footer className="bg-white dark:bg-gray-900 py-12 px-6 md:px-12">
<div className="container mx-auto max-w-6xl">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Logo and Description */}
<div className="col-span-1 md:col-span-2">
<a
href="#home"
className="flex items-center gap-2 mb-4"
onClick={(e) => {
e.preventDefault();
scrollToSection("home");
}}
>
<div className="flex items-center">
<img src="./ucefLogo.png" alt="Logo" className="h-14 w-14" />
<span className="text-2xl font-bold gradient-text bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">
Youssef Sahih
</span>
</div>
</a>
<p className="text-gray-600 dark:text-gray-400 mt-4 mb-6 max-w-md">
A passionate software developer with expertise in both low-level
programming and web development technologies.
</p>
<div className="flex space-x-3">
{socialLinks.map((link, index) => (
<motion.a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors"
whileHover={{ y: -3 }}
whileTap={{ scale: 0.95 }}
>
<link.icon className="w-5 h-5" />
</motion.a>
))}
</div>
</div>
{/* Quick Links */}
<div className="col-span-1">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">
Quick Links
</h3>
<ul className="space-y-2">
{navItems.map((item) => (
<li key={item.id}>
<a
href={`#${item.id}`}
className="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors"
onClick={(e) => {
e.preventDefault();
scrollToSection(item.id);
}}
>
{item.label}
</a>
</li>
))}
</ul>
</div>
{/* Contact Info */}
<div className="col-span-1">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">
Contact
</h3>
<ul className="space-y-2">
<li>
<a
href="mailto:ucefsahih@gmail.com"
className="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors"
>
ucefsahih@gmail.com
</a>
</li>
<li>
<a
href="tel:+212708978739"
className="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors"
>
+212 708 978 739
</a>
</li>
<li className="text-gray-600 dark:text-gray-400">Morocco</li>
</ul>
</div>
</div>
<div className="mt-12 pt-6 border-t border-gray-200 dark:border-gray-800 flex flex-col md:flex-row justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 text-sm">
&copy; {new Date().getFullYear()} Youssef Sahih. All rights
reserved.
</p>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-2 md:mt-0">
Designed & Built with <span className="text-red-500"></span> by
Youssef Sahih
</p>
</div>
</div>
{/* Scroll to top button */}
<motion.button
className={`fixed bottom-6 right-6 p-3 rounded-full bg-primary text-white shadow-lg ${
showScrollTop ? "opacity-100" : "opacity-0 pointer-events-none"
} transition-opacity duration-300`}
onClick={scrollToTop}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
aria-label="Scroll to top"
>
<ArrowUpIcon className="w-5 h-5" />
</motion.button>
</footer>
);
};
export default Footer;

View File

@ -0,0 +1,246 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ArrowDownIcon, DownloadIcon, GithubIcon, LinkedinIcon, MailIcon } from 'lucide-react';
interface HeroProps {
data: {
name: string;
about: string;
avatarUrl: string;
contact: {
email: string;
social: {
name: string;
url: string;
icon: any;
}[];
};
};
}
// Simple typewriter effect
const useTypewriter = (text: string, speed: number = 100) => {
const [displayText, setDisplayText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
const [isDone, setIsDone] = useState(false);
useEffect(() => {
if (currentIndex < text.length) {
const timeout = setTimeout(() => {
setDisplayText(prev => prev + text[currentIndex]);
setCurrentIndex(prev => prev + 1);
}, speed);
return () => clearTimeout(timeout);
} else {
setIsDone(true);
}
}, [currentIndex, speed, text]);
return { displayText, isDone };
};
const HeroSection: React.FC<HeroProps> = ({ data }) => {
const { displayText, isDone } = useTypewriter(data.about, 30);
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100
}
}
};
return (
<div className="min-h-screen flex items-center justify-center px-4 sm:px-6 md:px-12 py-20 md:py-32 relative overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-secondary/5 dark:from-primary/10 dark:to-secondary/10 -z-20" />
{/* Animated particles - Add pointer-events-none to prevent blocking clicks */}
<div className="absolute inset-0 -z-10 pointer-events-none">
{Array.from({ length: 20 }).map((_, i) => (
<motion.div
key={i}
className="absolute bg-primary-light/20 dark:bg-primary/30 rounded-full pointer-events-none"
style={{
width: Math.random() * 8 + 4,
height: Math.random() * 8 + 4,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
animate={{
y: [0, Math.random() * 30 - 15],
x: [0, Math.random() * 30 - 15],
opacity: [0.2, 0.5, 0.2],
}}
transition={{
duration: Math.random() * 5 + 5,
repeat: Infinity,
repeatType: "reverse",
}}
/>
))}
</div>
<div className="container mx-auto max-w-6xl relative z-10">
<div className="flex flex-col md:flex-row items-center justify-between gap-12">
{/* Left content */}
<motion.div
className="w-full md:w-7/12 text-center md:text-left"
variants={containerVariants}
initial="hidden"
animate="visible"
>
<motion.h1
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-4"
variants={itemVariants}
>
Hello, I&apos;m <span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary inline-block">{data.name}</span>
</motion.h1>
<motion.h2
className="text-xl md:text-2xl lg:text-3xl font-medium mb-6 text-gray-600 dark:text-gray-300"
variants={itemVariants}
>
Software Developer
</motion.h2>
<motion.div
className="mb-8 text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-xl"
variants={itemVariants}
>
<p className="min-h-[100px]">
{displayText}
{!isDone && <span className="inline-block w-1 h-6 ml-1 bg-primary animate-pulse"></span>}
</p>
</motion.div>
<motion.div
className="flex flex-wrap gap-4 justify-center md:justify-start"
variants={itemVariants}
>
<motion.a
href={`mailto:${data.contact.email}`}
className="px-6 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg shadow-lg shadow-primary/20 hover:shadow-primary/30 transition-all duration-300 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<MailIcon className="w-4 h-4" />
Contact Me
</motion.a>
<motion.a
href="/resume.pdf"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 border border-gray-300 dark:border-gray-600 hover:border-primary hover:text-primary dark:hover:border-primary-light dark:hover:text-primary-light rounded-lg transition-all duration-300 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<DownloadIcon className="w-4 h-4" />
Resume
</motion.a>
</motion.div>
<motion.div
className="mt-8 flex gap-4 justify-center md:justify-start"
variants={itemVariants}
>
{data.contact.social.map((social, index) => (
<motion.a
key={index}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-full bg-surface-light dark:bg-surface-dark hover:text-primary dark:hover:text-primary-light transition-colors duration-300"
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.9 }}
>
<social.icon className="w-5 h-5" />
</motion.a>
))}
</motion.div>
</motion.div>
{/* Right content - Avatar */}
<motion.div
className="w-full md:w-5/12"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="relative mx-auto w-64 h-64 md:w-80 md:h-80">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-primary to-secondary opacity-20 blur-xl animate-pulse" />
<motion.div
className="relative w-full h-full rounded-full p-2 bg-gradient-to-br from-primary to-secondary"
animate={{
boxShadow: [
'0 0 20px rgba(59, 130, 246, 0.3)',
'0 0 30px rgba(59, 130, 246, 0.5)',
'0 0 20px rgba(59, 130, 246, 0.3)',
],
}}
transition={{
duration: 3,
repeat: Infinity,
repeatType: 'reverse',
}}
>
<div className="w-full h-full rounded-full overflow-hidden bg-white dark:bg-background-dark p-1">
<img
src={data.avatarUrl}
alt={data.name}
className="w-full h-full rounded-full object-cover"
/>
</div>
</motion.div>
</div>
</motion.div>
</div>
</div>
{/* Scroll down indicator */}
<motion.div
className="absolute bottom-4 transform -translate-x-1/2 flex flex-col items-center"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1, duration: 0.5 }}
>
<motion.span
className="text-sm mb-2 opacity-60"
animate={{ y: [0, 5, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
Scroll Down
</motion.span>
<motion.div
className="bg-primary dark:bg-primary-light rounded-full p-1"
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
<ArrowDownIcon className="w-4 h-4 text-white dark:text-background-dark" />
</motion.div>
</motion.div>
</div>
);
};
export default HeroSection;

View File

@ -0,0 +1,189 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { useTheme } from "../Contexts/ThemeContext";
import { SunIcon, MoonIcon, Menu, X } from "lucide-react";
interface NavigationBarProps {
activeSection: string;
}
const NavigationBar: React.FC<NavigationBarProps> = ({ activeSection }) => {
const { theme, toggleTheme } = useTheme();
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const navItems = [
{ id: "home", label: "Home" },
{ id: "about", label: "About" },
{ id: "skills", label: "Skills" },
{ id: "experience", label: "Experience" },
{ id: "projects", label: "Projects" },
{ id: "education", label: "Education" },
{ id: "contact", label: "Contact" },
];
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const scrollToSection = (sectionId: string) => {
setIsMobileMenuOpen(false);
const element = document.getElementById(sectionId);
if (element) {
window.scrollTo({
top: element.offsetTop - 80,
behavior: "smooth",
});
}
};
return (
<motion.nav
className={`fixed top-0 left-0 right-0 z-50 px-4 sm:px-6 md:px-12 transition-all duration-300 ${
isScrolled
? "bg-white/80 dark:bg-background-dark/80 shadow-md backdrop-blur-md"
: "bg-transparent"
}`}
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="container mx-auto flex justify-between items-center">
<motion.a
href="#home"
className="text-2xl font-bold flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={(e) => {
e.preventDefault();
scrollToSection("home");
}}
>
<div className="flex items-center">
<img
src="./ucefLogo.png"
alt="Logo"
className="h-14 w-14"
/>
<span className="gradient-text bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">
Youssef
</span>
</div>
</motion.a>
{/* Desktop Menu */}
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<motion.a
key={item.id}
href={`#${item.id}`}
className={`text-sm font-medium transition-colors duration-300 hover:text-primary relative ${
activeSection === item.id
? "text-primary dark:text-primary-light"
: "text-gray-600 dark:text-gray-300"
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={(e) => {
e.preventDefault();
scrollToSection(item.id);
}}
>
{item.label}
{activeSection === item.id && (
<motion.span
className="absolute -bottom-1 left-0 right-0 h-0.5 bg-primary dark:bg-primary-light"
layoutId="navIndicator"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>
)}
</motion.a>
))}
<motion.button
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
className="p-2 rounded-full bg-surface-light dark:bg-surface-dark hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-300"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleTheme}
>
{theme === "dark" ? (
<SunIcon className="h-5 w-5 text-yellow-400" />
) : (
<MoonIcon className="h-5 w-5 text-gray-600" />
)}
</motion.button>
</div>
{/* Mobile Menu Button */}
<div className="flex items-center space-x-4 md:hidden">
<motion.button
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
className="p-2 rounded-full bg-surface-light dark:bg-surface-dark hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-300"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleTheme}
>
{theme === "dark" ? (
<SunIcon className="h-5 w-5 text-yellow-400" />
) : (
<MoonIcon className="h-5 w-5 text-gray-600" />
)}
</motion.button>
<motion.button
className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-300"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</motion.button>
</div>
</div>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<motion.div
className="absolute top-full left-0 right-0 bg-white dark:bg-background-dark shadow-lg rounded-b-lg py-4 md:hidden"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<div className="flex flex-col space-y-4 px-6">
{navItems.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`py-2 text-base font-medium transition-colors duration-300 hover:text-primary ${
activeSection === item.id
? "text-primary dark:text-primary-light"
: "text-gray-600 dark:text-gray-300"
}`}
onClick={(e) => {
e.preventDefault();
scrollToSection(item.id);
}}
>
{item.label}
</a>
))}
</div>
</motion.div>
)}
</motion.nav>
);
};
export default NavigationBar;

View File

@ -0,0 +1,266 @@
"use client";
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ExternalLinkIcon, GithubIcon, CodeIcon } from 'lucide-react';
interface Project {
title: string;
description: string;
techStack: string[];
link?: {
href: string;
label?: string;
};
}
interface ProjectsSectionProps {
projects: Project[];
}
const ProjectsSection: React.FC<ProjectsSectionProps> = ({ projects }) => {
const [activeFilter, setActiveFilter] = useState('All');
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
// Extract unique tech stacks for filter
const techStacks = Array.from(
new Set(projects.flatMap(project => project.techStack))
);
// Filter projects based on tech stack
const filteredProjects = activeFilter === 'All'
? projects
: projects.filter(project => project.techStack.includes(activeFilter));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
when: "beforeChildren"
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
},
},
};
// Generate a random image for each project (for demo purposes)
// In a real project, you'd use actual project images
const getProjectImage = (title: string) => {
const seed = title.charCodeAt(0) + title.length;
return `https://picsum.photos/seed/${seed}/600/400`;
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 overflow-hidden" id="projects">
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">My Projects</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
A showcase of my technical projects and applications, demonstrating my skills and expertise in various technologies.
</p>
</motion.div>
{/* Filter controls */}
<motion.div
className="flex flex-wrap justify-center gap-3 mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<motion.button
className={`px-4 py-2 rounded-full transition-all duration-300 ${
activeFilter === 'All'
? "bg-primary text-white shadow-md shadow-primary/20"
: "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => setActiveFilter('All')}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
All Projects
</motion.button>
{techStacks.map((tech, index) => (
<motion.button
key={index}
className={`px-4 py-2 rounded-full transition-all duration-300 ${
activeFilter === tech
? "bg-primary text-white shadow-md shadow-primary/20"
: "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => setActiveFilter(tech)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{tech}
</motion.button>
))}
</motion.div>
{/* Projects Grid */}
<motion.div
key={activeFilter}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{filteredProjects.map((project, index) => (
<motion.div
key={`${project.title}-${index}`}
layout
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300"
whileHover={{ y: -10 }}
onHoverStart={() => setHoveredProject(project.title)}
onHoverEnd={() => setHoveredProject(null)}
onClick={() => setSelectedProject(project)}
>
<div className="relative overflow-hidden h-48">
<img
src={getProjectImage(project.title)}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-500"
style={{
transform: hoveredProject === project.title ? 'scale(1.1)' : 'scale(1)'
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end p-4">
<h3 className="text-white text-xl font-bold">{project.title}</h3>
</div>
</div>
<div className="p-5">
<p className="text-gray-600 dark:text-gray-300 mb-4 line-clamp-3">
{project.description}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{project.techStack.map((tech, techIndex) => (
<span
key={techIndex}
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full"
>
{tech}
</span>
))}
</div>
<div className="flex justify-between items-center">
<button
className="text-primary dark:text-primary-light font-medium hover:underline flex items-center gap-1"
onClick={(e) => {
e.stopPropagation();
setSelectedProject(project);
}}
>
<CodeIcon className="w-4 h-4" />
View Details
</button>
{project.link && (
<a
href={project.link.href}
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors"
onClick={(e) => e.stopPropagation()}
>
<GithubIcon className="w-5 h-5" />
</a>
)}
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Project Modal */}
<AnimatePresence>
{selectedProject && (
<motion.div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedProject(null)}
>
<motion.div
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: "spring", damping: 20 }}
onClick={(e) => e.stopPropagation()}
>
<div className="relative h-64 md:h-80">
<img
src={getProjectImage(selectedProject.title)}
alt={selectedProject.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex flex-col justify-end p-6">
<h3 className="text-white text-2xl md:text-3xl font-bold">{selectedProject.title}</h3>
<div className="flex flex-wrap gap-2 mt-2">
{selectedProject.techStack.map((tech, techIndex) => (
<span
key={techIndex}
className="px-2 py-1 text-xs bg-white/20 text-white rounded-full"
>
{tech}
</span>
))}
</div>
</div>
</div>
<div className="p-6">
<p className="text-gray-600 dark:text-gray-300 mb-6">
{selectedProject.description}
</p>
{selectedProject.link && (
<div className="flex justify-end">
<a
href={selectedProject.link.href}
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 bg-primary text-white rounded-lg flex items-center gap-2 hover:bg-primary-dark transition-colors"
>
<GithubIcon className="w-4 h-4" />
View on GitHub
</a>
</div>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
export default ProjectsSection;

View File

@ -0,0 +1,236 @@
"use client";
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Code2Icon, ServerIcon, LayoutDashboardIcon, PenToolIcon } from 'lucide-react';
interface SkillsProps {
skills: string[];
}
// Skill proficiency data (this would normally come from a backend/database)
const SKILL_PROFICIENCY: Record<string, number> = {
"C": 95,
"C++": 90,
"JavaScript": 85,
"TypeScript": 80,
"Html": 90,
"Tailwind Css": 85,
"React": 80,
"Node.js/Next.js": 75,
"Docker": 70,
"Git": 85,
"Jira": 80,
};
// Categorized skills
const SKILL_CATEGORIES = {
"Languages": ["C", "C++", "JavaScript", "TypeScript"],
"Frontend": ["Html", "Tailwind Css", "React", "Redux"],
"Backend": ["Node.js/Next.js", "Docker"],
"DevOps & Tools": ["Git", "Docker", "Jira"]
};
const SkillsSection: React.FC<SkillsProps> = ({ skills }) => {
const [activeCategory, setActiveCategory] = useState<string>("All");
const categories = [
{ id: "All", label: "All Skills", icon: <Code2Icon className="w-5 h-5" /> },
{ id: "Languages", label: "Languages", icon: <Code2Icon className="w-5 h-5" /> },
{ id: "Frontend", label: "Frontend", icon: <LayoutDashboardIcon className="w-5 h-5" /> },
{ id: "Backend", label: "Backend", icon: <ServerIcon className="w-5 h-5" /> },
{ id: "DevOps & Tools", label: "DevOps & Tools", icon: <PenToolIcon className="w-5 h-5" /> },
];
const filteredSkills = activeCategory === "All"
? Object.values(SKILL_CATEGORIES).flat() // get all skills from categories
: SKILL_CATEGORIES[activeCategory as keyof typeof SKILL_CATEGORIES] || [];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
when: "beforeChildren"
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: "easeOut"
},
},
};
// Function to get devicon class
const getDeviconClass = (skill: string): string => {
const skillLower = skill.toLowerCase().replace(/\./g, '').replace(/\s/g, '');
// Map special cases
const skillMap: Record<string, string> = {
"c": "c",
"c++": "cplusplus",
"html": "html5",
"tailwind css": "tailwindcss",
"javascript": "javascript",
"typescript": "typescript",
"react": "react",
"redux": "redux",
"nodejs/nextjs": "nextjs",
"jira": "jira",
"docker": "docker",
"git": "git",
};
return skillMap[skillLower] || skillLower;
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 bg-surface-light dark:bg-surface-dark overflow-hidden" id="skills">
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">My Skills</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
A curated collection of my technical skills and proficiencies developed through education, personal projects, and hands-on experience.
</p>
</motion.div>
{/* Category Filter */}
<motion.div
className="flex flex-wrap justify-center gap-3 mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
{categories.map((category) => (
<motion.button
key={category.id}
className={`px-4 py-2 rounded-full flex items-center gap-2 transition-all duration-300 ${
activeCategory === category.id
? "bg-primary text-white shadow-md shadow-primary/20"
: "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => setActiveCategory(category.id)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{category.icon}
{category.label}
</motion.button>
))}
</motion.div>
{/* Skills Grid */}
<motion.div
key={activeCategory}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
animate="visible"
viewport={{ once: true, margin: "-100px" }}
>
{filteredSkills.map((skill, index) => {
const proficiency = SKILL_PROFICIENCY[skill] || 75;
const deviconClass = getDeviconClass(skill);
return (
<motion.div
key={`${skill}-${index}`}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg hover:shadow-xl transition-shadow duration-300"
variants={itemVariants}
initial="hidden"
animate="visible"
whileHover={{ y: -5 }}
>
<div className="flex items-center mb-4">
<div className="text-3xl text-primary mr-3">
<i className={`devicon-${deviconClass}-plain colored`}></i>
</div>
<h3 className="text-xl font-semibold">{skill}</h3>
</div>
<div className="relative h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<motion.div
className="absolute top-0 left-0 h-full bg-gradient-to-r from-primary to-secondary rounded-full"
initial={{ width: 0 }}
whileInView={{ width: `${proficiency}%` }}
viewport={{ once: true }}
transition={{ duration: 1, delay: 0.2, ease: "easeOut" }}
/>
</div>
<div className="flex justify-between mt-2 text-sm text-gray-600 dark:text-gray-400">
<span>Proficiency</span>
<span>{proficiency}%</span>
</div>
</motion.div>
);
})}
</motion.div>
{/* Additional Skills Section */}
<motion.div
className="mt-16 bg-white dark:bg-gray-800 rounded-lg p-8 shadow-lg"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<h3 className="text-2xl font-semibold mb-6 text-center">Additional Skills & Methodologies</h3>
<div className="flex flex-wrap justify-center gap-3">
{[
// "Git",
// "Docker",
"Linux",
"Socket Programming",
"Project Management",
"Problem Solving",
"Agile Methodology",
"Scrum",
// "Jira",
"Team Collaboration",
"CI/CD",
"Code Review",
"Test-Driven Development",
"RESTful APIs",
"System Design",
"Technical Documentation"
].map((skill, index) => (
<motion.span
key={index}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 rounded-full text-gray-800 dark:text-gray-200"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
whileHover={{
scale: 1.05,
backgroundColor: "rgba(59, 130, 246, 0.1)",
color: "rgb(59, 130, 246)"
}}
>
{skill}
</motion.span>
))}
</div>
</motion.div>
</div>
</div>
);
};
export default SkillsSection;

19
app/components/icons.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
export const GitHubIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
export const LinkedInIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
export const XIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<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" />
</svg>
);

164
app/data/resume-data.ts Normal file
View File

@ -0,0 +1,164 @@
import { GitHubIcon, LinkedInIcon, XIcon } from "../components/icons";
export const RESUME_DATA = {
name: "Youssef Sahih",
location: "Morocco",
about:
"Motivated software developer with a strong track record of successfully completing various projects. Proficient in various programming languages and technologies, including C, C++, and web development technologies (HTML, CSS, JavaScript). Skilled in areas such as file handling, multithreading, game development, virtualization, and networking. Strong problem-solving abilities and a solid understanding of software development principles. Committed to continuous learning and delivering high-quality solutions. Ready to contribute expertise to new challenges.",
summary:
"As a software Developer with a background in computer systems, algorithms, and data structures, I have two years of experience in IT and two years of English studies under my belt. I am currently furthering my education in the 42 network for a Master Digital IT Architect degree. Coding and problem-solving through code are my passions, and I am excited to collaborate with talented programmers and expand my knowledge even more!",
avatarUrl: "./ysahih.png",
contact: {
email: "ucefsahih@gmail.com",
tel: "+212708978739",
social: [
{
name: "GitHub",
url: "https://github.com/ysahih",
icon: GitHubIcon,
},
{
name: "LinkedIn",
url: "https://www.linkedin.com/in/youssef-sahih/",
icon: LinkedInIcon,
},
{
name: "X",
url: "https://x.com/uc3f02",
icon: XIcon,
},
],
},
education: [
{
school: "Université Chouaïb Doukkali",
degree: "Associate's Degree in English Studies.",
start: "2020",
end: "2022",
},
{
school: "1337 - (42 network)",
degree: "Master's Degree in IT Architecture.",
start: "2022",
end: "present",
},
],
skills: [
"C",
"C++",
"Html",
"Tailwind Css",
"JavaScript",
"TypeScript",
"React",
"Redux",
"Node.js/Next.js",
"Docker",
"Git",
"Jira",
"Agile/Scrum",
],
projects: [
{
title: "Pongy",
techStack: ["Next.js", "React", "TypeScript"],
description:
"Pong contest website with real-time multiplayer games, chat, and security",
link: {
label: "github.com/ysahih",
href: "https://github.com/ysahih/PingPong",
},
},
{
title: "Wordle-Game clone",
techStack: ["Html", "Css", "JavaScript"],
description: "a simple clone of the famous wordle game",
link: {
label: "github.com",
href: "https://github.com/ysahih/blog",
},
},
{
title: "Inception",
techStack: ["Docker", "Nginx", "mariaDb", "Wordpress"],
description:
"Created a Docker-based multi-container infrastructure with Nginx, WordPress, and MariaDB for a web application.",
link: {
label: "github.com",
href: "https://github.com/ysahih/inception",
},
},
{
title: "IRC",
techStack: ["C++", "Socket Programming"],
description:
"Internet Relay Chat server (Communication protocol on the Internet)",
link: {
label: "github.com",
href: "https://github.com/ysahih/IRC",
},
},
{
title: "cub3D",
techStack: ["C", "Graphics"],
description:
"My first RayCaster with miniLibX. This project is inspired by the world-famous Wolfenstein 3D game.",
link: {
label: "github.com",
href: "https://github.com/ysahih/cub3D",
},
},
{
title: "sash",
techStack: ["C", "Software Design and Architecture"],
description: "simple implementation of Unix Shell with C",
link: {
label: "github.com",
href: "https://github.com/ysahih/Sash",
},
},
],
experience: [
{
company: "Im'enSe",
position: "Frontend Developer",
logo: 'https://media.licdn.com/dms/image/v2/C4E0BAQGwBEMaD6MB4Q/company-logo_200_200/company-logo_200_200/0/1670258446221?e=1749686400&v=beta&t=ke4p83O45mws8hx8rE5qkaD8KH5-aI1hPa6gesmqx_U',
location: "Tangier, Morocco",
startDate: "Aug 2024",
endDate: "Present",
description:
"Working on front-end development projects for clients in the QHSE sector, focusing on secure, scalable web applications.",
skills: [
"React",
"TypeScript",
"Redux",
"Node.js",
"Jira",
"Agile",
"Git",
"CI/CD",
"REST APIs",
],
achievements: [
"Collaborated with cross-functional teams to deliver projects on time",
"Contributed to the development of a secure authentication system",
"Conducted code reviews and provided constructive feedback to peers",
"Participated in the design and architecture of a new web application",
"Worked closely with UX/UI designers to implement user-friendly interfaces",
"Contributed to the development of a reusable component library",
"Participated in the migration of a legacy application to a modern tech stack",
"Worked on a project that reduced page load time by 50%",
"Participated in the development of a web application that supports multiple languages",
"Worked on a project that improved accessibility for users with disabilities",
"Worked on a project that improved SEO performance by 30%",
"Participated in the development of a web application that integrates with third-party APIs",
"Worked on a project that improved data visualization for users",
],
},
],
} as const;

100
app/globals.css Normal file
View File

@ -0,0 +1,100 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background-light: 0 0% 100%;
--background-dark: 222.2 84% 4.9%;
--text-light: 222.2 47.4% 11.2%;
--text-dark: 210 40% 98%;
--primary: 221.2 83.2% 53.3%;
--primary-dark: 222.2 47.4% 11.2%;
--primary-light: 210 40% 98%;
--secondary: 142.1 76.2% 36.3%;
--secondary-dark: 142.1 70.6% 45.3%;
--secondary-light: 138 76.5% 96.7%;
}
html {
scroll-behavior: smooth;
}
body {
@apply min-h-screen font-sans antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-sans;
}
}
@layer components {
.container {
@apply mx-auto px-4 sm:px-6 lg:px-8;
}
.gradient-text {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary;
}
.glass-card {
@apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg;
}
}
/* Animations */
@keyframes floating {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
@keyframes pulse-slow {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.animate-floating {
animation: floating 5s ease-in-out infinite;
}
.animate-pulse-slow {
animation: pulse-slow 3s ease-in-out infinite;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
@apply bg-gray-100 dark:bg-gray-800;
}
::-webkit-scrollbar-thumb {
@apply bg-primary/50 dark:bg-primary/70 rounded-full;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-primary/70 dark:bg-primary/90;
}
/* Focus styles for accessibility */
:focus-visible {
@apply outline-none ring-2 ring-primary/50 ring-offset-2 ring-offset-white dark:ring-offset-gray-900;
}
/* Smooth theme transition */
* {
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

28
app/layout.tsx Normal file
View File

@ -0,0 +1,28 @@
"use client";
import './globals.css';
import { ThemeProvider } from './Contexts/ThemeContext';
import { Inter } from 'next/font/google';
import { Toaster } from 'react-hot-toast';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
</head>
<body className={`${inter.className} antialiased transition-colors duration-300 bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark`}>
<ThemeProvider>
<Toaster position="top-right" />
{children}
</ThemeProvider>
</body>
</html>
);
}

105
app/not-found.tsx Normal file
View File

@ -0,0 +1,105 @@
"use client";
import Link from 'next/link';
import { motion } from 'framer-motion';
import { HomeIcon } from 'lucide-react';
export default function NotFound() {
return (
<div className="h-screen w-full bg-gray-900 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-gray-900 via-gray-900 to-black flex items-center justify-center relative overflow-hidden text-white">
{/* Stars background */}
<div className="absolute inset-0">
{Array.from({ length: 100 }).map((_, i) => {
const size = Math.random() * 2 + 1;
const opacity = Math.random() * 0.5 + 0.5;
const animationDuration = Math.random() * 5 + 3;
return (
<motion.div
key={i}
className="absolute bg-white rounded-full z-0"
style={{
width: size,
height: size,
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
opacity
}}
animate={{
opacity: [opacity, opacity * 0.5, opacity]
}}
transition={{
duration: animationDuration,
repeat: Infinity,
repeatType: "reverse"
}}
/>
);
})}
</div>
{/* Content */}
<div className="text-center z-10 px-6 max-w-2xl">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="mb-8"
>
<h1 className="text-9xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-purple-400 to-blue-600">
404
</h1>
</motion.div>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Lost in Space
</h2>
<p className="text-gray-300 text-lg mb-8">
The page you are looking for might have been moved, deleted, or perhaps never existed in this universe.
</p>
<div className="flex justify-center">
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Link
href="/"
className="bg-gradient-to-r from-purple-500 to-blue-500 text-white px-6 py-3 rounded-full flex items-center space-x-2 shadow-lg transition-all duration-300 hover:shadow-purple-500/25"
>
<HomeIcon className="w-5 h-5" />
<span>Return Home</span>
</Link>
</motion.div>
</div>
</motion.div>
{/* Floating Astronaut */}
<motion.div
className="absolute w-32 h-32 opacity-80"
style={{ rotate: 15 }}
animate={{
y: [0, -15, 0],
x: [0, 10, 0],
rotate: [15, 20, 15]
}}
transition={{
duration: 4,
repeat: Infinity,
repeatType: "reverse"
}}
>
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C7.59 2 4 5.59 4 10C4 13.91 6.72 17.14 10.39 17.85C10.25 18.23 10 18.93 10 19.5C10 20.88 11.12 22 12.5 22C13.88 22 15 20.88 15 19.5C15 18.93 14.75 18.23 14.61 17.85C18.28 17.14 21 13.91 21 10C21 5.59 17.41 2 13 2H12ZM12 4C15.33 4 18 6.67 18 10C18 13.33 15.33 16 12 16C8.67 16 6 13.33 6 10C6 6.67 8.67 4 12 4ZM7 7.5C6.17 7.5 5.5 8.17 5.5 9C5.5 9.83 6.17 10.5 7 10.5C7.83 10.5 8.5 9.83 8.5 9C8.5 8.17 7.83 7.5 7 7.5ZM17 7.5C16.17 7.5 15.5 8.17 15.5 9C15.5 9.83 16.17 10.5 17 10.5C17.83 10.5 18.5 9.83 18.5 9C18.5 8.17 17.83 7.5 17 7.5Z" />
</svg>
</motion.div>
</div>
</div>
);
}

163
app/page.tsx Normal file
View File

@ -0,0 +1,163 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import NavigationBar from './components/NavigationBar';
import HeroSection from './components/HeroSection';
import AboutSection from './components/AboutSection';
import SkillsSection from './components/SkillsSection';
import ProjectsSection from './components/ProjectsSection';
import EducationSection from './components/EducationSection';
import ContactSection from './components/ContactSection';
import Footer from './components/Footer';
import { useTheme } from './Contexts/ThemeContext';
import { RESUME_DATA } from './data/resume-data';
import ExperienceSection from './components/ExperienceSection';
export default function Portfolio() {
const { theme } = useTheme();
const [activeSection, setActiveSection] = useState('home');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate loading to ensure smooth animations
const timer = setTimeout(() => {
setIsLoading(false);
}, 1000);
// Add scroll event listener for setting active section
const handleScroll = () => {
const sections = document.querySelectorAll('section[id]');
const scrollPosition = window.scrollY + 100;
sections.forEach((section) => {
const sectionTop = (section as HTMLElement).offsetTop;
const sectionHeight = (section as HTMLElement).offsetHeight;
const sectionId = section.getAttribute('id') || '';
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
setActiveSection(sectionId);
}
});
};
window.addEventListener('scroll', handleScroll);
return () => {
clearTimeout(timer);
window.removeEventListener('scroll', handleScroll);
};
}, []);
if (isLoading) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center"
>
<motion.img
src="./ucefLogo.png"
alt="Logo"
className="w-20 h-20"
initial={{ opacity: 0, scale: 0.5 }}
animate={{
opacity: 1,
scale: 1,
rotate: [0, 10, -10, 10, 0],
y: [0, -10, 0]
}}
transition={{
duration: 2.5,
repeat: Infinity,
repeatType: "reverse",
ease: [0.25, 0.1, 0.25, 1]
}}
/>
<motion.p
className="-mt-4 text-gray-700 dark:text-gray-300 text-lg font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">
Loading Portfolio...
</span>
</motion.p>
</motion.div>
</div>
);
}
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: "easeOut"
}
}
};
return (
<div className="min-h-screen flex flex-col bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark overflow-x-hidden">
<NavigationBar activeSection={activeSection} />
<motion.main
className="flex-grow"
initial="hidden"
animate="visible"
variants={{
visible: {
transition: {
staggerChildren: 0.1
}
}
}}
>
<motion.section id="home" variants={fadeInUp}>
<HeroSection data={RESUME_DATA as any} />
</motion.section>
<motion.section id="about" variants={fadeInUp}>
<AboutSection data={RESUME_DATA} />
</motion.section>
<motion.section id="skills" variants={fadeInUp}>
<SkillsSection skills={[...RESUME_DATA.skills]} />
</motion.section>
<motion.section id="experience" variants={fadeInUp}>
<ExperienceSection experience={RESUME_DATA.experience} />
</motion.section>
<motion.section id="projects" variants={fadeInUp}>
<ProjectsSection
projects={RESUME_DATA.projects.map(project => ({
...project,
techStack: [...project.techStack]
}))}
/>
</motion.section>
<motion.section id="education" variants={fadeInUp}>
<EducationSection education={[...RESUME_DATA.education]} />
</motion.section>
<motion.section id="contact" variants={fadeInUp}>
<ContactSection
contact={{
...RESUME_DATA.contact,
social: [...RESUME_DATA.contact.social]
}}
location={RESUME_DATA.location}
/>
</motion.section>
</motion.main>
<Footer socialLinks={[...RESUME_DATA.contact.social]} />
</div>
);
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

10
next.config.mjs Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['picsum.photos'],
},
reactStrictMode: true,
swcMinify: true,
};
export default nextConfig;

9212
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/material": "^5.15.15",
"@radix-ui/themes": "^3.0.3",
"@react-pdf/renderer": "^3.4.2",
"autoprefixer": "^10.4.21",
"bootstrap": "^5.3.3",
"class-variance-authority": "^0.7.0",
"devicon": "^2.16.0",
"devicons-react": "^1.4.1",
"emailjs-com": "^3.2.0",
"framer-motion": "^12.6.3",
"lucide-react": "^0.376.0",
"next": "^14.2.3",
"react": "^18",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.1.0",
"styled-components": "^6.1.9",
"tailwind-merge": "^2.3.0",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/webfontloader": "^1.6.38",
"eslint": "^8",
"eslint-config-next": "14.2.2",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/ucefLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/ysahih.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

75
tailwind.config.js Normal file
View File

@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#3B82F6', // Blue
dark: '#2563EB',
light: '#60A5FA',
},
secondary: {
DEFAULT: '#10B981', // Green
dark: '#059669',
light: '#34D399',
},
background: {
light: '#FFFFFF',
dark: '#111827',
},
surface: {
light: '#F3F4F6',
dark: '#1F2937',
},
text: {
light: '#1F2937',
dark: '#F9FAFB',
}
},
fontFamily: {
sans: ['Inter var', 'Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.5s ease-in-out',
'slide-down': 'slideDown 0.5s ease-in-out',
'slide-in-right': 'slideInRight 0.5s ease-in-out',
'slide-in-left': 'slideInLeft 0.5s ease-in-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideInRight: {
'0%': { transform: 'translateX(-20px)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
slideInLeft: {
'0%': { transform: 'translateX(20px)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
},
backdropFilter: {
'none': 'none',
'blur': 'blur(20px)',
},
},
},
plugins: [],
}

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}