diff --git a/.gitignore b/.gitignore index bd5f5d1..2d13bcd 100755 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +./src/alphabetical.py diff --git a/app/Contexts/ThemeContext.tsx b/app/Contexts/ThemeContext.tsx new file mode 100644 index 0000000..8a37237 --- /dev/null +++ b/app/Contexts/ThemeContext.tsx @@ -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(undefined); + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState('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 ( + + {children} + + ); +}; + +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; diff --git a/app/components/AboutSection.tsx b/app/components/AboutSection.tsx new file mode 100644 index 0000000..e7b2901 --- /dev/null +++ b/app/components/AboutSection.tsx @@ -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 = ({ 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: , + title: "Problem Solver", + description: "Approaching complex coding challenges with analytical thinking and persistence." + }, + { + icon: , + title: "Continuous Learner", + description: "Passionate about staying updated with the latest technologies and best practices." + }, + { + icon: , + title: "Creative Thinker", + description: "Finding innovative solutions by thinking outside the box." + }, + { + icon: , + title: "Academic Focus", + description: "Pursuing a Master's Degree in IT Architecture in the 42 network." + } + ]; + + return ( +
+ {/* Background shape */} +
+
+ +
+ +

About Me

+
+ + +
+ {/* Left side - Image */} + +
+
+
+
+

Who I Am

+

+ {data.summary} +

+
+

+ Currently pursuing my passion for programming in the 42 network, working toward a Master's Degree in IT Architecture. +

+
+
+
+ + + {/* Right side - Qualities */} + +

My Qualities

+
+ {qualities.map((quality, index) => ( + +
+ {quality.icon} +
+

{quality.title}

+

+ {quality.description} +

+
+ ))} +
+ + +

+ I am passionate about solving problems through code and building applications that provide genuine value. +

+
+
+
+
+
+ ); +}; + +export default AboutSection; \ No newline at end of file diff --git a/app/components/ContactSection.tsx b/app/components/ContactSection.tsx new file mode 100644 index 0000000..b00da86 --- /dev/null +++ b/app/components/ContactSection.tsx @@ -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 = ({ contact, location }) => { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }); + const [formStatus, setFormStatus] = useState('idle'); + const [errors, setErrors] = useState>({}); + + const validateForm = (): boolean => { + const newErrors: Partial = {}; + + 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) => { + 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 ( +
+
+ +

Get In Touch

+
+

+ I'm always open to discussing new projects, creative ideas, or opportunities to be part of your vision. +

+ + +
+ {/* Contact Info */} + +

Contact Information

+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+

Location

+

{location}

+
+
+
+ + +

Connect with me

+
+ {contact.social.map((platform, index) => ( + + + + ))} +
+
+
+ + {/* Contact Form */} + +
+

Send Me a Message

+ +
+
+ + + {errors.name &&

{errors.name}

} +
+ +
+ + + {errors.email &&

{errors.email}

} +
+ +
+ + + {errors.subject &&

{errors.subject}

} +
+ +
+ +