├── src ├── vite-env.d.ts ├── index.css ├── assets │ └── images │ │ ├── f.png │ │ ├── under.png │ │ ├── fotoCurso.jpg │ │ ├── google-sc.png │ │ ├── keepInShape.png │ │ ├── microsoft-ai.png │ │ ├── microsoft-sc.png │ │ ├── python-ess.png │ │ ├── webScraper.png │ │ ├── californiaDreaming.png │ │ └── flightFeelAnalizer.png ├── main.tsx ├── components │ ├── UnderConstruction.tsx │ ├── Loader.tsx │ ├── ProjectsCard.tsx │ ├── DetailModal.tsx │ ├── Navbar.tsx │ ├── FeatureCarrousel.tsx │ ├── Footer.tsx │ └── ContactForm.tsx ├── pages │ ├── Contact.tsx │ ├── Projects.tsx │ ├── Home.tsx │ └── About.tsx ├── routes │ └── Router.tsx ├── layout │ └── Layout.tsx └── data │ └── ProjectsData.ts ├── vite.config.ts ├── tsconfig.json ├── .gitignore ├── README.md ├── tsconfig.node.json ├── index.html ├── tsconfig.app.json ├── eslint.config.js ├── LICENSE └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | html { 4 | scroll-behavior: smooth; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/f.png -------------------------------------------------------------------------------- /src/assets/images/under.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/under.png -------------------------------------------------------------------------------- /src/assets/images/fotoCurso.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/fotoCurso.jpg -------------------------------------------------------------------------------- /src/assets/images/google-sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/google-sc.png -------------------------------------------------------------------------------- /src/assets/images/keepInShape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/keepInShape.png -------------------------------------------------------------------------------- /src/assets/images/microsoft-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/microsoft-ai.png -------------------------------------------------------------------------------- /src/assets/images/microsoft-sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/microsoft-sc.png -------------------------------------------------------------------------------- /src/assets/images/python-ess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/python-ess.png -------------------------------------------------------------------------------- /src/assets/images/webScraper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/webScraper.png -------------------------------------------------------------------------------- /src/assets/images/californiaDreaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/californiaDreaming.png -------------------------------------------------------------------------------- /src/assets/images/flightFeelAnalizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omarlsant/omarlsant-portfolio/HEAD/src/assets/images/flightFeelAnalizer.png -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tailwindcss from '@tailwindcss/vite' 3 | export default defineConfig({ 4 | plugins: [ 5 | tailwindcss(), 6 | ], 7 | base: "/omarlsant-portfolio/", 8 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "moduleResolution": "bundler", 4 | "skipLibCheck": true, 5 | 6 | "references": [ 7 | { "path": "./tsconfig.app.json" }, 8 | { "path": "./tsconfig.node.json" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | public/ 27 | .gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mi Portafolio Digital 🚀 2 | 3 | ¡Bienvenido/a a mi portafolio! Aquí encontrarás una selección de mis proyectos más recientes y destacados. 4 | 5 | Este portafolio ha sido creado con React + Vite (Tailwind CSS, Router-dom, Zustand) para mostrar mis habilidades y experiencia en desarrollo web. 6 | 7 | **Explora los diferentes proyectos para conocer más sobre mi trabajo.** 8 | 9 | --- 10 | 11 | ¡Gracias por visitar! -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RouterProvider } from 'react-router-dom'; 4 | import { router } from './routes/Router'; 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (rootElement) { 9 | ReactDOM.createRoot(rootElement).render( 10 | 11 | 12 | 13 | ); 14 | } else { 15 | console.error("Root element not found"); 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Omar Lengua's Portfolio 8 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/UnderConstruction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import constructionImage from '../assets/images/under.png'; 4 | 5 | const UnderConstruction: React.FC = () => { 6 | return ( 7 |
8 |
9 | Website Under Construction - We are currently working on this page 14 | 15 |
16 | 17 | Go Back Home 18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default UnderConstruction; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Omar Lengua Suárez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/pages/Contact.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/Contact.tsx 2 | 3 | import React from 'react'; 4 | import ContactForm from '../components/ContactForm'; // Ensure the path is correct 5 | 6 | const Contact: React.FC = () => { 7 | return ( 8 | // Main page/section container 9 |
10 |
11 | 12 | {/* Section Title */} 13 |

14 | Get in Touch 15 |

16 | {/* Optional introductory text */} 17 |

18 | Have a question or want to work together? Fill out the form, and I'll get back to you as soon as possible. 19 |

20 | 21 | {/* Form container with background and shadow */} 22 |
23 | {/* Render the form component */} 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Contact; -------------------------------------------------------------------------------- /src/routes/Router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from "react-router-dom"; 2 | import Layout from "../layout/Layout"; 3 | import Home from "../pages/Home"; 4 | import About from "../pages/About"; 5 | import Projects from "../pages/Projects"; 6 | import Contact from "../pages/Contact"; 7 | import UnderConstruction from "../components/UnderConstruction"; 8 | 9 | const basename = "/omarlsant-portfolio"; 10 | 11 | export const router = createBrowserRouter([ 12 | { 13 | path: "/", 14 | element: , 15 | children: [ 16 | { 17 | index: true, 18 | element: 19 | }, 20 | { 21 | path: "about", 22 | element: , 23 | }, 24 | { 25 | path: "projects", 26 | element: , 27 | }, 28 | { 29 | path: "contact", 30 | element: , 31 | }, 32 | { 33 | path: "in-construction", 34 | element: , 35 | }, 36 | { 37 | path: "*", 38 | element:
App 404: Page Not Found in Router
, 39 | } 40 | ] 41 | } 42 | ], 43 | 44 | { basename: basename } 45 | 46 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omarlsant-porfolio", 3 | "private": true, 4 | "homepage": "https://omarlsant.github.io/omarlsant-portfolio", 5 | "version": "0.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "lint": "eslint .", 11 | "preview": "vite preview", 12 | "predeploy": "npm run build", 13 | "deploy": "gh-pages -d dist" 14 | }, 15 | "dependencies": { 16 | "@emotion/react": "^11.14.0", 17 | "@emotion/styled": "^11.14.0", 18 | "@mui/icons-material": "^7.0.1", 19 | "@mui/material": "^7.0.1", 20 | "@tailwindcss/vite": "^4.0.17", 21 | "@tsparticles/react": "^3.0.0", 22 | "gh-pages": "^6.3.0", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "react-hook-form": "^7.55.0", 26 | "react-icons": "^5.5.0", 27 | "react-router-dom": "^7.4.0", 28 | "react-tsparticles": "^2.12.2", 29 | "react-type-animation": "^3.2.0", 30 | "swiper": "^11.2.8", 31 | "tailwindcss": "^4.0.17", 32 | "tsparticles": "^3.8.1" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.21.0", 36 | "@types/node": "^22.14.1", 37 | "@types/react": "^19.0.10", 38 | "@types/react-dom": "^19.0.4", 39 | "@types/react-type-animation": "^1.1.4", 40 | "@vitejs/plugin-react-swc": "^3.8.0", 41 | "eslint": "^9.21.0", 42 | "eslint-plugin-react-hooks": "^5.1.0", 43 | "eslint-plugin-react-refresh": "^0.4.19", 44 | "globals": "^15.15.0", 45 | "typescript": "~5.7.2", 46 | "typescript-eslint": "^8.24.1", 47 | "vite": "^6.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const LOADER_DISPLAY_DURATION = 1000; 4 | const FADE_OUT_DURATION = 200; 5 | 6 | const Loader: React.FC<{ onFinish: () => void }> = ({ onFinish }) => { 7 | const [fadeOut, setFadeOut] = useState(false); 8 | 9 | useEffect(() => { 10 | const fadeTimer = setTimeout(() => { 11 | setFadeOut(true); 12 | }, LOADER_DISPLAY_DURATION); 13 | 14 | const finishTimer = setTimeout(() => { 15 | onFinish(); 16 | }, LOADER_DISPLAY_DURATION + FADE_OUT_DURATION); 17 | 18 | 19 | return () => { 20 | clearTimeout(fadeTimer); 21 | clearTimeout(finishTimer); 22 | }; 23 | }, [onFinish]); 24 | 25 | return ( 26 |
31 | {/* Fondo Animado */} 32 |
33 | 34 | {/* Texto "Loading" */} 35 | 38 | Loading... 39 | 40 | 41 | {/* Spinner */} 42 |
{/* Añadir z-10 */} 43 |
44 | ); 45 | }; 46 | 47 | export default Loader; -------------------------------------------------------------------------------- /src/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import Navbar from '../components/Navbar'; 4 | import Footer from '../components/Footer'; 5 | import Loader from '../components/Loader'; 6 | import Particles, { initParticlesEngine } from "@tsparticles/react"; 7 | import { type ISourceOptions } from "@tsparticles/engine"; 8 | import { loadFull } from "tsparticles"; 9 | 10 | const particleOptions: ISourceOptions = { 11 | fpsLimit: 60, 12 | interactivity: { 13 | events: { onClick: { enable: true, mode: 'push' }, onHover: { enable: true, mode: 'repulse' } }, 14 | modes: { push: { quantity: 4 }, repulse: { distance: 100, duration: 0.4 } }, 15 | }, 16 | particles: { 17 | color: { value: '#ffffff' }, 18 | links: { color: '#ffffff', distance: 150, enable: true, opacity: 0.2, width: 1 }, 19 | move: { direction: 'none', enable: true, outModes: { default: 'bounce' }, random: false, speed: 1, straight: false }, 20 | number: { density: { enable: true }, value: 50 }, 21 | opacity: { value: 0.3 }, 22 | shape: { type: 'circle' }, 23 | size: { value: { min: 1, max: 3 } }, 24 | }, 25 | detectRetina: true, 26 | background: { 27 | color: '#111827', 28 | } 29 | }; 30 | 31 | const Layout = () => { 32 | const [showLoader, setShowLoader] = useState(true); 33 | const [particlesInitialized, setParticlesInitialized] = useState(false); 34 | 35 | useEffect(() => { 36 | if (particlesInitialized) return; 37 | initParticlesEngine(async (engine) => { 38 | await loadFull(engine); 39 | }).then(() => { 40 | setParticlesInitialized(true); 41 | }).catch(err => { 42 | console.error("Error initializing particles engine:", err); 43 | setParticlesInitialized(true); 44 | }); 45 | }, [particlesInitialized]); 46 | 47 | const handleLoaderFinish = useCallback(() => { 48 | setShowLoader(false); 49 | }, []); 50 | 51 | const particlesLoaded = useCallback(async () => { 52 | console.log('Particles container loaded'); 53 | }, []); 54 | 55 | if (showLoader || !particlesInitialized) { 56 | return ; 57 | } 58 | 59 | return ( 60 |
61 | {particlesInitialized && ( 62 | 68 | )} 69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default Layout; -------------------------------------------------------------------------------- /src/components/ProjectsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Project } from '../data/ProjectsData'; 3 | import { FaGithub } from 'react-icons/fa'; 4 | 5 | interface ProjectCardProps { 6 | project: Project; 7 | onDetailsClick: (project: Project) => void; 8 | } 9 | 10 | const ProjectCard: React.FC = ({ project, onDetailsClick }) => { 11 | const { title, description, repoUrl, type } = project; 12 | 13 | const typeBadgeClasses = type === 'Group' 14 | ? 'bg-teal-400/10 text-teal-300 border-teal-400/30' // Style for Group 15 | : 'bg-sky-400/10 text-sky-300 border-sky-400/30'; // Style for Individual 16 | 17 | return ( 18 |
26 |

{title}

27 |

28 | {description} 29 |

30 | 31 |
32 |
33 | 44 | 45 | GitHub Repo 46 | 47 | 58 |
59 | 60 | 63 | {type} {/* This will now correctly display 'Group' or 'Individual' */} 64 | 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default ProjectCard; -------------------------------------------------------------------------------- /src/components/DetailModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Project, ProjectType } from '../data/ProjectsData'; 3 | 4 | interface ProjectDetailModalProps { 5 | project: Project; 6 | onClose: () => void; 7 | } 8 | 9 | const ProjectDetailModal: React.FC = ({ project, onClose }) => { 10 | if (!project) return null; 11 | 12 | const { title, description, repoUrl, type, category, technologies, detailedDescription } = project; 13 | 14 | const handleContentClick = (e: React.MouseEvent) => { 15 | e.stopPropagation(); 16 | }; 17 | 18 | // Update conditional check for 'Group' 19 | const typeBadgeClasses = type === ('Group' as ProjectType) 20 | ? 'bg-teal-400/10 text-teal-300 border-teal-400/30' // Group style 21 | : 'bg-sky-400/10 text-sky-300 border-sky-400/30'; // Individual style 22 | 23 | return ( 24 |
28 |
36 | 45 | 46 |
47 |

{title}

48 |
49 | 50 | {type} 51 | 52 | 53 | Category: {category} 54 | 55 |
56 |
57 | 58 |
59 |

{detailedDescription || description}

60 |
61 | 62 | {technologies && technologies.length > 0 && ( 63 |
64 |

65 | Technologies Used: 66 |

67 |
68 | {technologies.map((tech, index) => ( 69 | 73 | {tech} 74 | 75 | ))} 76 |
77 |
78 | )} 79 | 80 | 93 |
94 |
95 | ); 96 | }; 97 | 98 | export default ProjectDetailModal; -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { NavLink, Link } from 'react-router-dom'; 3 | import { Menu as MenuIcon, Close as CloseIcon } from '@mui/icons-material'; 4 | import logoimg from '../assets/images/f.png'; 5 | 6 | const navLinks = [ 7 | { name: 'Home', path: '/' }, 8 | { name: 'About me', path: '/about' }, 9 | { name: 'Projects', path: '/projects' } 10 | ]; 11 | 12 | const scrollToTop = () => { 13 | window.scrollTo({ top: 0, behavior: 'smooth' }); 14 | }; 15 | 16 | const Navbar = () => { 17 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 18 | 19 | const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen); 20 | const closeMobileMenu = () => setIsMobileMenuOpen(false); 21 | 22 | const handleLogoClick = () => { 23 | closeMobileMenu(); 24 | scrollToTop(); 25 | }; 26 | 27 | const linkClasses = "px-3 py-2 rounded-md text-sm font-medium transition-colors duration-300 ease-in-out block sm:inline-block"; 28 | const activeLinkClasses = "bg-gray-700/70 text-white"; 29 | const inactiveLinkClasses = "text-gray-300 hover:bg-gray-700/50 hover:text-white"; 30 | 31 | return ( 32 | 104 | ); 105 | }; 106 | 107 | export default Navbar; -------------------------------------------------------------------------------- /src/pages/Projects.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { projectsData, Project } from '../data/ProjectsData'; 3 | import ProjectCard from '../components/ProjectsCard'; 4 | import ProjectDetailModal from '../components/DetailModal'; 5 | 6 | const ProjectsPage: React.FC = () => { 7 | const [showScrollButton, setShowScrollButton] = useState(false); 8 | 9 | const [selectedProject, setSelectedProject] = useState(null); 10 | const aiProjects = projectsData.filter(p => p.category === 'AI Developer'); 11 | const fullStackProjects = projectsData.filter(p => p.category === 'Full Stack Developer'); 12 | 13 | const handleOpenDetails = (project: Project) => { 14 | setSelectedProject(project); 15 | document.body.style.overflow = 'hidden'; 16 | }; 17 | 18 | const handleCloseModal = () => { 19 | setSelectedProject(null); 20 | document.body.style.overflow = 'auto'; 21 | }; 22 | 23 | const renderCategorySection = (title: string, projects: Project[], highlight: string) => ( 24 |
25 |

26 | {highlight} {title.replace(highlight, '').trim()} 27 |

28 |
29 | {projects.map((project) => ( 30 | 35 | ))} 36 |
37 |
38 | ); 39 | 40 | useEffect(() => { 41 | const checkScrollTop = () => { 42 | if (!showScrollButton && window.scrollY > 400) { 43 | setShowScrollButton(true); 44 | } else if (showScrollButton && window.scrollY <= 400) { 45 | setShowScrollButton(false); 46 | } 47 | }; 48 | window.addEventListener('scroll', checkScrollTop); 49 | return () => window.removeEventListener('scroll', checkScrollTop); 50 | }, [showScrollButton]); 51 | 52 | const scrollToTop = () => { 53 | window.scrollTo({ top: 0, behavior: 'smooth' }); 54 | }; 55 | 56 | return ( 57 |
58 |
59 |

60 | My Projects 61 |

62 |

63 | Explore the projects I've worked on, spanning from Data Analysis and Artificial Intelligence to Full Stack Development. 64 |

65 |
66 | 67 |
68 | {/* Render AI section */} 69 | {renderCategorySection('Developer', aiProjects, 'AI')} 70 | {/* Render Full Stack section */} 71 | {renderCategorySection('Developer', fullStackProjects, 'Full Stack')} 72 |
73 | 74 | {selectedProject && ( 75 | 79 | )} 80 | 81 | {showScrollButton && ( 82 | 90 | )} 91 |
92 | ); 93 | }; 94 | 95 | export default ProjectsPage; -------------------------------------------------------------------------------- /src/components/FeatureCarrousel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Swiper, SwiperSlide } from 'swiper/react'; 3 | import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules'; 4 | import 'swiper/css'; 5 | import 'swiper/css/navigation'; 6 | import 'swiper/css/pagination'; 7 | import 'swiper/css/effect-coverflow'; 8 | import 'swiper/css/autoplay'; 9 | 10 | import { FaExternalLinkAlt } from 'react-icons/fa'; 11 | 12 | export type ProjectType = 'Group' | 'Individual'; 13 | 14 | interface CarouselProject { 15 | id: string; 16 | title: string; 17 | description: string; 18 | imageUrl: string; 19 | link: string; 20 | type: ProjectType; 21 | } 22 | 23 | interface FeaturedProjectsCarouselProps { 24 | projects: CarouselProject[]; 25 | } 26 | 27 | const FeaturedProjectsCarousel: React.FC = ({ projects }) => { 28 | if (!projects || projects.length === 0) { 29 | return

No projects to display.

; 30 | } 31 | 32 | return ( 33 |
34 |

Main Projects

35 | 1} // Solo activar loop si hay más de un proyecto 55 | className="mySwiper w-full pb-10" // pb-10 para espacio de paginación 56 | breakpoints={{ 57 | 640: { 58 | slidesPerView: 1, 59 | spaceBetween: 20, 60 | }, 61 | 768: { 62 | slidesPerView: 2, 63 | spaceBetween: 30, 64 | }, 65 | 1024: { 66 | slidesPerView: 'auto', // Dejar que el ancho del slide defina cuántos caben 67 | spaceBetween: 40, 68 | }, 69 | }} 70 | > 71 | {projects.map((project) => ( 72 | 76 | {project.title} 81 |
{/* Un poco más de padding */} 82 |

{project.title}

{/* truncar título largo */} 83 |

84 | {project.description} 85 |

86 | 93 | Watch the repo 94 | 95 |
96 |
97 | ))} 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default FeaturedProjectsCarousel; -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { FaGithub, FaLinkedin } from 'react-icons/fa'; 3 | 4 | const scrollToTop = () => { 5 | window.scrollTo({ top: 0, behavior: 'smooth' }); 6 | }; 7 | 8 | interface FooterLink { 9 | label: string; 10 | href: string; 11 | isExternal?: boolean; 12 | icon?: React.ElementType; 13 | } 14 | 15 | const navigationLinks: FooterLink[] = [ 16 | { label: 'Home', href: '/' }, 17 | { label: 'About me', href: '/about' }, 18 | { label: 'Projects', href: '/projects' }, 19 | ]; 20 | 21 | const socialLinks: FooterLink[] = [ 22 | { label: 'GitHub', href: 'https://github.com/Omarlsant', isExternal: true, icon: FaGithub }, 23 | { label: 'LinkedIn', href: 'https://www.linkedin.com/in/omarlengua/', isExternal: true, icon: FaLinkedin }, 24 | ]; 25 | 26 | 27 | interface LinkColumnProps { 28 | title: string; 29 | links: FooterLink[]; 30 | } 31 | 32 | const LinkColumn: React.FC = ({ title, links }) => ( 33 |
34 |

{title}

35 |
    36 | {links.map((link) => { 37 | const isHomeLink = !link.isExternal && link.href === '/'; 38 | return ( 39 |
  • 40 | {link.isExternal ? ( 41 | 48 | {link.icon && } 49 | {link.label} 50 | 51 | ) : ( 52 | 57 | {link.label} 58 | 59 | )} 60 |
  • 61 | ); 62 | })} 63 |
64 |
65 | ); 66 | 67 | 68 | const Footer = () => { 69 | return ( 70 |
71 |
72 |
73 |
74 | 75 |
76 |

Social links

77 | 93 |
94 |
95 | 96 |
97 |

Email

98 | 99 | omarns21@gmail.com 100 | 101 |

Location

102 |

103 | Madrid, Spain. 104 |

105 |
106 |
107 | 108 |
109 |
110 |

111 | © {new Date().getFullYear()} Omar Lengua. All rights reserved. 112 |

113 |
114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export default Footer; -------------------------------------------------------------------------------- /src/components/ContactForm.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ContactForm.tsx 2 | import React, { useState } from 'react'; 3 | 4 | interface FormData { 5 | name: string; 6 | email: string; 7 | message: string; 8 | } 9 | 10 | const ContactForm: React.FC = () => { 11 | const [formData, setFormData] = useState({ 12 | name: '', 13 | email: '', 14 | message: '', 15 | }); 16 | const [isSubmitting, setIsSubmitting] = useState(false); 17 | const [submitMessage, setSubmitMessage] = useState(''); 18 | const [submitSuccess, setSubmitSuccess] = useState(null); 19 | 20 | const handleChange = ( 21 | e: React.ChangeEvent 22 | ) => { 23 | const { name, value } = e.target; 24 | setFormData((prevData) => ({ 25 | ...prevData, 26 | [name]: value, 27 | })); 28 | }; 29 | 30 | const handleSubmit = async (e: React.FormEvent) => { 31 | e.preventDefault(); 32 | setIsSubmitting(true); 33 | setSubmitMessage(''); 34 | setSubmitSuccess(null); 35 | 36 | setTimeout(() => { 37 | setSubmitMessage("Thank you for your message! I'll be in touch soon."); 38 | setSubmitSuccess(true); 39 | setIsSubmitting(false); 40 | setFormData({ name: '', email: '', message: '' }); 41 | 42 | 43 | }, 2000); 44 | }; 45 | 46 | return ( 47 |
48 |
49 | 55 |
56 | 68 |
69 |
70 | 71 |
72 | 78 |
79 | 91 |
92 |
93 | 94 |
95 | 101 |
102 |