diff --git a/src/App.jsx b/src/App.jsx index b67887f..c009a9b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ // src/App.js -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import GlobalStyles from './styles/GlobalStyles'; import Header from './Components/Header'; import AboutMe from './Components/AboutMe'; @@ -13,8 +13,31 @@ import Footer from './Components/Footer'; import GithubRepos from './Components/GithubRepos'; import ProjectsWithGithub from './Components/ProjectsWithGithub'; import ScrollReveal from './Components/ScrollReveal'; +import DarkModeToggle from './Components/DarkModeToggle'; +import BackToTop from './Components/BackToTop'; function App() { + const [isDark, setIsDark] = useState(() => { + try { + return localStorage.getItem('darkMode') === 'true'; + } catch { + return false; + } + }); + + useEffect(() => { + if (isDark) { + document.body.classList.add('dark-mode'); + } else { + document.body.classList.remove('dark-mode'); + } + try { + localStorage.setItem('darkMode', isDark); + } catch { + // ignore storage errors + } + }, [isDark]); + useEffect(() => { let ticking = false; @@ -50,8 +73,12 @@ function App() { return ( <> + setIsDark(d => !d)} /> + + + @@ -69,6 +96,7 @@ function App() { + > ); } diff --git a/src/Components/AboutMe.jsx b/src/Components/AboutMe.jsx index d573679..c2a45a4 100644 --- a/src/Components/AboutMe.jsx +++ b/src/Components/AboutMe.jsx @@ -6,10 +6,9 @@ import styled from 'styled-components'; const AboutSection = styled.section` padding: 4rem 0; - background-color: #f9f9f9; - color: #333; + background-color: var(--section-bg, #f9f9f9); + color: var(--text-color, #333); text-align: center; - margin-top: 80px; // Leave space for header `; const AboutContent = styled.div` diff --git a/src/Components/BackToTop.jsx b/src/Components/BackToTop.jsx new file mode 100644 index 0000000..8ce5d5c --- /dev/null +++ b/src/Components/BackToTop.jsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { FaArrowUp } from 'react-icons/fa'; + +const BackToTopButton = styled.button` + position: fixed; + bottom: 2rem; + right: 1.5rem; + z-index: 999; + background: #ff6f61; + color: #fff; + border: none; + border-radius: 50%; + width: 48px; + height: 48px; + font-size: 1.2rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + opacity: ${({ $visible }) => ($visible ? 1 : 0)}; + pointer-events: ${({ $visible }) => ($visible ? 'auto' : 'none')}; + transform: translateY(${({ $visible }) => ($visible ? '0' : '20px')}); + transition: opacity 0.3s, transform 0.3s, background 0.2s; + + &:hover { + background: #e65a51; + } +`; + +const BackToTop = () => { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setVisible(window.scrollY > 300); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( + + + + ); +}; + +export default BackToTop; diff --git a/src/Components/DarkModeToggle.jsx b/src/Components/DarkModeToggle.jsx new file mode 100644 index 0000000..c1e0a8d --- /dev/null +++ b/src/Components/DarkModeToggle.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import styled from 'styled-components'; +import { FaSun, FaMoon } from 'react-icons/fa'; + +const ToggleButton = styled.button` + position: fixed; + top: 1.2rem; + right: 1.5rem; + z-index: 1000; + background: var(--toggle-bg, #333); + color: var(--toggle-color, #fff); + border: none; + border-radius: 50px; + padding: 0.5rem 1rem; + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + transition: background 0.3s, color 0.3s, box-shadow 0.3s; + + &:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + } +`; + +const DarkModeToggle = ({ isDark, onToggle }) => { + return ( + + {isDark ? : } + + ); +}; + +export default DarkModeToggle; diff --git a/src/Components/Header.jsx b/src/Components/Header.jsx index 4b5a6ec..055fc2d 100644 --- a/src/Components/Header.jsx +++ b/src/Components/Header.jsx @@ -1,8 +1,42 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { FaGithub, FaLinkedin, FaEnvelope } from 'react-icons/fa'; import profilePic from '../images/profile-pic.png'; +const TYPING_PHRASES = [ + 'AI Integration & Cloud Computing', + 'Full-Stack Software Developer', + 'Building Impactful Solutions', +]; + +const useTypewriter = (phrases, typingSpeed = 80, deletingSpeed = 40, pauseMs = 1800) => { + const [text, setText] = useState(''); + const [phraseIndex, setPhraseIndex] = useState(0); + const [isDeleting, setIsDeleting] = useState(false); + + useEffect(() => { + const current = phrases[phraseIndex % phrases.length]; + let timeout; + + if (!isDeleting && text === current) { + timeout = setTimeout(() => setIsDeleting(true), pauseMs); + } else if (isDeleting && text === '') { + setIsDeleting(false); + setPhraseIndex(i => i + 1); + } else { + timeout = setTimeout(() => { + setText(prev => + isDeleting ? prev.slice(0, -1) : current.slice(0, prev.length + 1) + ); + }, isDeleting ? deletingSpeed : typingSpeed); + } + + return () => clearTimeout(timeout); + }, [text, isDeleting, phraseIndex, phrases, typingSpeed, deletingSpeed, pauseMs]); + + return text; +}; + const HeaderSection = styled.section` display: flex; flex-direction: column; @@ -10,7 +44,7 @@ const HeaderSection = styled.section` justify-content: center; text-align: center; padding: 3rem; - background-color: #f8f8f8; + background-color: var(--bg-color, #f8f8f8); min-height: 80vh; position: relative; overflow: hidden; @@ -112,20 +146,36 @@ const ProfileImage = styled.img` const Name = styled.h1` font-size: 2.5rem; font-weight: bold; - color: #333; + color: var(--text-color, #333); margin-bottom: 1rem; position: relative; z-index: 1; `; -const Description = styled.p` +const TypingText = styled.p` font-size: 1.2rem; - color: #555; + color: var(--subtext-color, #555); line-height: 1.6; max-width: 600px; margin-bottom: 2rem; position: relative; z-index: 1; + min-height: 2em; +`; + +const Cursor = styled.span` + display: inline-block; + width: 2px; + height: 1.1em; + background: #ff6f61; + margin-left: 2px; + vertical-align: text-bottom; + animation: blink 0.8s step-end infinite; + + @keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } + } `; const ButtonContainer = styled.div` @@ -182,6 +232,8 @@ const ConnectSection = styled.div` `; const Header = () => { + const typedText = useTypewriter(TYPING_PHRASES); + return ( @@ -193,10 +245,9 @@ const Header = () => { Yasar Nazzarian - - I'm a passionate software Developer with experience in AI integration, - cloud computing, and software development. Let's create impactful solutions together. - + + {typedText} + View Projects diff --git a/src/Components/Skills.jsx b/src/Components/Skills.jsx index 9f5c913..5535061 100644 --- a/src/Components/Skills.jsx +++ b/src/Components/Skills.jsx @@ -1,67 +1,111 @@ -import React from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import styled from 'styled-components'; import { FaJava, FaPython, FaJsSquare, FaReact, FaDocker, FaMicrosoft } from 'react-icons/fa'; // Import relevant icons +import { skills } from '../data/skills'; + +const ICONS = { + Java: , + Python: , + JavaScript: , + React: , + Docker: , + Azure: , +}; const SkillsSection = styled.section` - padding: 3rem 0; + padding: 3rem 2rem; text-align: center; + background-color: var(--section-bg, #f9f9f9); h2 { font-size: 2rem; margin-bottom: 2rem; + color: var(--text-color, #333); } .skills-container { display: flex; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; + flex-direction: column; + gap: 1.2rem; + max-width: 600px; + margin: 0 auto; } +`; - .skill { - display: flex; - align-items: center; - background-color: #f0f0f0; - padding: 0.8rem 1.5rem; - border-radius: 15px; - font-size: 1.2rem; - transition: background-color 0.3s; - cursor: pointer; +const SkillRow = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.4rem; +`; - &:hover { - background-color: #e0e0e0; - } +const SkillLabel = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.05rem; + color: var(--text-color, #333); - svg { - margin-right: 0.5rem; - font-size: 1.5rem; - } + svg { + font-size: 1.3rem; + color: #ff6f61; } `; +const LevelPercentage = styled.span` + margin-left: auto; + color: #ff6f61; + font-size: 0.9rem; +`; + +const BarTrack = styled.div` + width: 100%; + background-color: var(--card-bg, #e0e0e0); + border-radius: 8px; + height: 10px; + overflow: hidden; +`; + +const BarFill = styled.div` + height: 100%; + border-radius: 8px; + background: linear-gradient(90deg, #ff6f61, #ff9a8b); + width: ${({ $animated, $level }) => ($animated ? `${$level}%` : '0%')}; + transition: width 1s ease-out; +`; + const Skills = () => { + const sectionRef = useRef(null); + const [animated, setAnimated] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setAnimated(true); + observer.disconnect(); + } + }, + { threshold: 0.3 } + ); + if (sectionRef.current) observer.observe(sectionRef.current); + return () => observer.disconnect(); + }, []); + return ( - + Skills - - Java - - - Python - - - JavaScript - - - React - - - Docker - - - Azure - + {skills.map(({ name, level }) => ( + + + {ICONS[name]} {name} {level}% + + + + + + ))} ); diff --git a/src/data/skills.js b/src/data/skills.js new file mode 100644 index 0000000..c1788c7 --- /dev/null +++ b/src/data/skills.js @@ -0,0 +1,11 @@ +// src/data/skills.js +const skills = [ + { name: 'Java', level: 85 }, + { name: 'Python', level: 90 }, + { name: 'JavaScript', level: 88 }, + { name: 'React', level: 82 }, + { name: 'Docker', level: 75 }, + { name: 'Azure', level: 78 }, +]; + +module.exports = { skills }; diff --git a/src/styles/GlobalStyles.js b/src/styles/GlobalStyles.js index 0509acf..3946f8d 100644 --- a/src/styles/GlobalStyles.js +++ b/src/styles/GlobalStyles.js @@ -5,6 +5,16 @@ import { createGlobalStyle } from 'styled-components'; const GlobalStyles = createGlobalStyle` :root { --scroll-progress: 0; + + /* Light mode defaults */ + --bg-color: #f8f8f8; + --text-color: #222; + --subtext-color: #666; + --card-bg: #f0f0f0; + --card-hover-bg: #e0e0e0; + --section-bg: #f9f9f9; + --toggle-bg: #333; + --toggle-color: #fff; } body { @@ -12,8 +22,20 @@ const GlobalStyles = createGlobalStyle` padding: 0; box-sizing: border-box; font-family: 'Arial', sans-serif; - background-color: #f8f8f8; - color: #222; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; + } + + body.dark-mode { + --bg-color: #121212; + --text-color: #e0e0e0; + --subtext-color: #aaa; + --card-bg: #1e1e1e; + --card-hover-bg: #2a2a2a; + --section-bg: #181818; + --toggle-bg: #e0e0e0; + --toggle-color: #121212; } body::before, @@ -65,10 +87,11 @@ const GlobalStyles = createGlobalStyle` h2 { font-size: 2.5rem; margin-bottom: 1.5rem; + color: var(--text-color); } p, small { - color: #666; + color: var(--subtext-color); } `; diff --git a/tests/skills.test.js b/tests/skills.test.js new file mode 100644 index 0000000..29f4d7f --- /dev/null +++ b/tests/skills.test.js @@ -0,0 +1,15 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { skills } = require('../src/data/skills'); + +test('skills entries include required fields', () => { + assert.ok(Array.isArray(skills)); + assert.ok(skills.length > 0); + + skills.forEach((skill) => { + assert.equal(typeof skill.name, 'string'); + assert.ok(skill.name.trim().length > 0); + assert.equal(typeof skill.level, 'number'); + assert.ok(skill.level >= 0 && skill.level <= 100); + }); +});