From a28728af2a47514870e3280fee33592116ef5bd2 Mon Sep 17 00:00:00 2001 From: Pablo de la Torre Jamardo Date: Tue, 22 Jul 2025 08:06:10 +0200 Subject: [PATCH] Initial functional version of the portfolio chatbot site --- app.vue | 6 +- package-lock.json | 122 +++++----- src/App.vue | 215 +++++++---------- src/components/chat/ChatFloatingButton.vue | 21 ++ src/components/chat/ChatPopup.vue | 148 ++++++++++++ src/components/layout/AppFooter.vue | 74 ++++++ src/components/layout/AppNavigation.vue | 72 ++++++ src/components/sections/AboutSection.vue | 65 +++++ src/components/sections/ContactSection.vue | 170 +++++++++++++ src/components/sections/ExperienceSection.vue | 92 +++++++ src/components/sections/HeroSection.vue | 85 +++++++ src/components/sections/ProjectsSection.vue | 99 ++++++++ src/components/sections/SkillsSection.vue | 75 ++++++ src/composables/useNavigation.js | 18 ++ src/composables/usePortfolioData.js | 30 +++ src/data/portfolio-config.json | 228 ++++++++++++++++++ .../repositories/ChatRepository.js | 34 +++ .../repositories/PortfolioRepository.js | 40 +++ src/services/ChatService.js | 77 ++++++ src/style.css | 6 +- tailwind.config.js | 12 +- 21 files changed, 1484 insertions(+), 205 deletions(-) create mode 100644 src/components/chat/ChatFloatingButton.vue create mode 100644 src/components/chat/ChatPopup.vue create mode 100644 src/components/layout/AppFooter.vue create mode 100644 src/components/layout/AppNavigation.vue create mode 100644 src/components/sections/AboutSection.vue create mode 100644 src/components/sections/ContactSection.vue create mode 100644 src/components/sections/ExperienceSection.vue create mode 100644 src/components/sections/HeroSection.vue create mode 100644 src/components/sections/ProjectsSection.vue create mode 100644 src/components/sections/SkillsSection.vue create mode 100644 src/composables/useNavigation.js create mode 100644 src/composables/usePortfolioData.js create mode 100644 src/data/portfolio-config.json create mode 100644 src/infrastructure/repositories/ChatRepository.js create mode 100644 src/infrastructure/repositories/PortfolioRepository.js create mode 100644 src/services/ChatService.js diff --git a/app.vue b/app.vue index e0c77cd..a561885 100644 --- a/app.vue +++ b/app.vue @@ -463,11 +463,13 @@ onMounted(() => { } ::-webkit-scrollbar-thumb { - background: rgba(147, 51, 234, 0.5); + /*background: rgba(147, 51, 234, 0.5);*/ + background: rgba(59, 130, 246, 0.5); /* Blue-500 */ border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(147, 51, 234, 0.7); + /*background: rgba(147, 51, 234, 0.7);*7 + background: rgba(59, 130, 246, 0.7); /* Blue-500 */ } diff --git a/package-lock.json b/package-lock.json index 73d6696..1e7c522 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,9 +72,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -882,39 +882,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", - "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", + "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@vue/shared": "3.5.17", + "@babel/parser": "^7.28.0", + "@vue/shared": "3.5.18", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", - "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", + "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-core": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", - "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", + "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@vue/compiler-core": "3.5.17", - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17", + "@babel/parser": "^7.28.0", + "@vue/compiler-core": "3.5.18", + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", @@ -922,63 +922,63 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", - "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", + "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-dom": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/reactivity": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", - "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", + "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.17" + "@vue/shared": "3.5.18" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", - "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", + "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/reactivity": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", - "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", + "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.17", - "@vue/runtime-core": "3.5.17", - "@vue/shared": "3.5.17", + "@vue/reactivity": "3.5.18", + "@vue/runtime-core": "3.5.18", + "@vue/shared": "3.5.18", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", - "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", + "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18" }, "peerDependencies": { - "vue": "3.5.17" + "vue": "3.5.18" } }, "node_modules/@vue/shared": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", - "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", "license": "MIT" }, "node_modules/ansi-regex": { @@ -1328,9 +1328,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", + "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", "dev": true, "license": "ISC" }, @@ -2909,16 +2909,16 @@ } }, "node_modules/vue": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", - "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", + "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-sfc": "3.5.17", - "@vue/runtime-dom": "3.5.17", - "@vue/server-renderer": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-sfc": "3.5.18", + "@vue/runtime-dom": "3.5.18", + "@vue/server-renderer": "3.5.18", + "@vue/shared": "3.5.18" }, "peerDependencies": { "typescript": "*" diff --git a/src/App.vue b/src/App.vue index 42143bc..0bb5722 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,156 +1,103 @@ diff --git a/src/components/chat/ChatFloatingButton.vue b/src/components/chat/ChatFloatingButton.vue new file mode 100644 index 0000000..423514e --- /dev/null +++ b/src/components/chat/ChatFloatingButton.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/chat/ChatPopup.vue b/src/components/chat/ChatPopup.vue new file mode 100644 index 0000000..7ab7b6d --- /dev/null +++ b/src/components/chat/ChatPopup.vue @@ -0,0 +1,148 @@ + + + diff --git a/src/components/layout/AppFooter.vue b/src/components/layout/AppFooter.vue new file mode 100644 index 0000000..53de7f3 --- /dev/null +++ b/src/components/layout/AppFooter.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/components/layout/AppNavigation.vue b/src/components/layout/AppNavigation.vue new file mode 100644 index 0000000..27c60e7 --- /dev/null +++ b/src/components/layout/AppNavigation.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/components/sections/AboutSection.vue b/src/components/sections/AboutSection.vue new file mode 100644 index 0000000..7b8d41e --- /dev/null +++ b/src/components/sections/AboutSection.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/sections/ContactSection.vue b/src/components/sections/ContactSection.vue new file mode 100644 index 0000000..61e76da --- /dev/null +++ b/src/components/sections/ContactSection.vue @@ -0,0 +1,170 @@ + + + diff --git a/src/components/sections/ExperienceSection.vue b/src/components/sections/ExperienceSection.vue new file mode 100644 index 0000000..11a12fa --- /dev/null +++ b/src/components/sections/ExperienceSection.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/components/sections/HeroSection.vue b/src/components/sections/HeroSection.vue new file mode 100644 index 0000000..fd96f88 --- /dev/null +++ b/src/components/sections/HeroSection.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/components/sections/ProjectsSection.vue b/src/components/sections/ProjectsSection.vue new file mode 100644 index 0000000..83e9944 --- /dev/null +++ b/src/components/sections/ProjectsSection.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/components/sections/SkillsSection.vue b/src/components/sections/SkillsSection.vue new file mode 100644 index 0000000..c206f79 --- /dev/null +++ b/src/components/sections/SkillsSection.vue @@ -0,0 +1,75 @@ + + + diff --git a/src/composables/useNavigation.js b/src/composables/useNavigation.js new file mode 100644 index 0000000..7514536 --- /dev/null +++ b/src/composables/useNavigation.js @@ -0,0 +1,18 @@ +export function useNavigation() { + function scrollToSection(sectionId) { + const element = document.getElementById(sectionId) + if (element) { + const offset = 80 // Account for fixed header + const elementPosition = element.offsetTop - offset + + window.scrollTo({ + top: elementPosition, + behavior: "smooth", + }) + } + } + + return { + scrollToSection, + } +} diff --git a/src/composables/usePortfolioData.js b/src/composables/usePortfolioData.js new file mode 100644 index 0000000..8a70dcb --- /dev/null +++ b/src/composables/usePortfolioData.js @@ -0,0 +1,30 @@ +import { ref } from "vue" +import { PortfolioRepository } from "@/infrastructure/repositories/PortfolioRepository" + +export function usePortfolioData() { + const portfolioData = ref({}) + const isLoading = ref(false) + const error = ref(null) + + const portfolioRepository = new PortfolioRepository() + + async function loadPortfolioData() { + try { + isLoading.value = true + error.value = null + portfolioData.value = await portfolioRepository.getPortfolioData() + } catch (err) { + error.value = err.message + console.error("Error loading portfolio data:", err) + } finally { + isLoading.value = false + } + } + + return { + portfolioData, + isLoading, + error, + loadPortfolioData, + } +} diff --git a/src/data/portfolio-config.json b/src/data/portfolio-config.json new file mode 100644 index 0000000..b080180 --- /dev/null +++ b/src/data/portfolio-config.json @@ -0,0 +1,228 @@ +{ + "personal": { + "name": "Pablo de la Torre Jamardo", + "title": "Senior Full Stack Developer", + "subtitle": "Especializado en Vue.js, React y Node.js", + "email": "pablo.delatorre@ejemplo.com", + "phone": "+54 11 1234-5678", + "location": "Buenos Aires, Argentina", + "avatar": "/src/assets/avatar-bot.png", + "bio": "Desarrollador Full Stack con más de 5 años de experiencia creando aplicaciones web escalables y modernas. Especializado en JavaScript, Vue.js, React y arquitecturas de microservicios.", + "social": { + "github": "https://github.com/pablo-delatorre", + "linkedin": "https://linkedin.com/in/pablo-delatorre", + "twitter": "https://twitter.com/pablo_dev", + "portfolio": "https://pablo-portfolio.com" + } + }, + "experience": [ + { + "id": "exp1", + "company": "TechCorp Solutions", + "position": "Senior Full Stack Developer", + "period": "2021 - Presente", + "location": "Buenos Aires, Argentina", + "description": "Liderazgo de equipo de 5 desarrolladores, arquitectura de microservicios con Node.js y Docker, implementación de CI/CD reduciendo deploys en 80%.", + "technologies": ["Vue.js", "Node.js", "Docker", "AWS", "PostgreSQL"], + "achievements": [ + "Migración de aplicaciones legacy a arquitecturas modernas", + "Reducción del 80% en tiempo de deployment", + "Implementación de testing automatizado" + ] + }, + { + "id": "exp2", + "company": "StartupXYZ", + "position": "Frontend Developer", + "period": "2019 - 2021", + "location": "Buenos Aires, Argentina", + "description": "Desarrollo de SPAs con Vue.js y React, optimización de performance y colaboración con equipos UX/UI.", + "technologies": ["Vue.js", "React", "TypeScript", "Tailwind CSS"], + "achievements": [ + "Optimización de Core Web Vitals", + "Implementación de design system", + "Mejora del 40% en performance" + ] + }, + { + "id": "exp3", + "company": "DevAgency", + "position": "Junior Developer", + "period": "2018 - 2019", + "location": "Buenos Aires, Argentina", + "description": "Desarrollo de APIs REST con Express.js, integración con bases de datos y metodologías ágiles.", + "technologies": ["Node.js", "Express.js", "MongoDB", "Git"], + "achievements": [ + "Desarrollo de 15+ APIs REST", + "Participación en metodologías ágiles", + "Contribución a proyectos open source" + ] + } + ], + "projects": [ + { + "id": "proj1", + "title": "E-commerce Platform", + "description": "Plataforma completa de comercio electrónico con más de 50,000 usuarios activos mensuales", + "image": "/placeholder.svg?height=300&width=400", + "technologies": ["Vue.js", "Node.js", "PostgreSQL", "Redis", "Stripe"], + "features": [ + "Integración con múltiples pasarelas de pago", + "Panel de administración con analytics", + "Sistema de inventario en tiempo real" + ], + "metrics": { + "users": "50,000+", + "uptime": "99.9%", + "loadTime": "2s" + }, + "links": { + "demo": "https://demo-ecommerce.com", + "github": "https://github.com/pablo/ecommerce" + } + }, + { + "id": "proj2", + "title": "Real-time Analytics Dashboard", + "description": "Dashboard empresarial con visualizaciones interactivas y procesamiento de más de 1M eventos/día", + "image": "/placeholder.svg?height=300&width=400", + "technologies": ["React", "D3.js", "Socket.io", "InfluxDB", "Node.js"], + "features": ["Visualizaciones en tiempo real", "Exportación de reportes automatizada", "Alertas personalizables"], + "metrics": { + "events": "1M+/día", + "reduction": "70% tiempo análisis", + "users": "500+" + }, + "links": { + "demo": "https://analytics-demo.com", + "github": "https://github.com/pablo/analytics" + } + }, + { + "id": "proj3", + "title": "Microservices Architecture", + "description": "Migración de monolito a arquitectura de microservicios con 12 servicios distribuidos", + "image": "/placeholder.svg?height=300&width=400", + "technologies": ["Node.js", "Docker", "Kubernetes", "AWS EKS", "MongoDB"], + "features": ["Event Sourcing y CQRS", "Monitoreo con Prometheus", "Auto-scaling automático"], + "metrics": { + "services": "12", + "latencyReduction": "60%", + "availability": "99.95%" + }, + "links": { + "github": "https://github.com/pablo/microservices" + } + } + ], + "skills": { + "frontend": [ + { "name": "Vue.js", "level": 95, "years": 4 }, + { "name": "React", "level": 90, "years": 3 }, + { "name": "TypeScript", "level": 85, "years": 3 }, + { "name": "Tailwind CSS", "level": 90, "years": 2 }, + { "name": "Nuxt.js", "level": 80, "years": 2 } + ], + "backend": [ + { "name": "Node.js", "level": 95, "years": 5 }, + { "name": "Express.js", "level": 90, "years": 4 }, + { "name": "NestJS", "level": 80, "years": 2 }, + { "name": "Python", "level": 75, "years": 2 }, + { "name": "GraphQL", "level": 70, "years": 1 } + ], + "database": [ + { "name": "PostgreSQL", "level": 85, "years": 4 }, + { "name": "MongoDB", "level": 80, "years": 3 }, + { "name": "Redis", "level": 75, "years": 2 } + ], + "devops": [ + { "name": "Docker", "level": 85, "years": 3 }, + { "name": "AWS", "level": 80, "years": 2 }, + { "name": "Kubernetes", "level": 70, "years": 1 }, + { "name": "CI/CD", "level": 85, "years": 3 } + ] + }, + "education": [ + { + "id": "edu1", + "institution": "Universidad Tecnológica Nacional", + "degree": "Ingeniería en Sistemas de Información", + "period": "2014 - 2018", + "description": "Especialización en Desarrollo de Software. Proyecto final: Sistema de gestión hospitalaria.", + "grade": "8.5/10" + } + ], + "certifications": [ + { + "id": "cert1", + "name": "AWS Certified Developer Associate", + "issuer": "Amazon Web Services", + "date": "2022", + "credentialId": "AWS-123456" + }, + { + "id": "cert2", + "name": "MongoDB Certified Developer", + "issuer": "MongoDB Inc.", + "date": "2021", + "credentialId": "MONGO-789012" + }, + { + "id": "cert3", + "name": "Certified Scrum Master", + "issuer": "Scrum Alliance", + "date": "2020", + "credentialId": "CSM-345678" + } + ], + "chatbot": { + "welcome": { + "message": "¡Hola! 👋 Soy el asistente virtual de Pablo. Puedo contarte sobre su experiencia, habilidades, proyectos y más. ¿Qué te gustaría saber?", + "quickActions": [ + { + "text": "¿Cuál es su experiencia laboral?", + "category": "experience" + }, + { + "text": "¿Qué tecnologías domina?", + "category": "skills" + }, + { + "text": "Cuéntame sobre sus proyectos", + "category": "projects" + }, + { + "text": "¿Cómo puedo contactarlo?", + "category": "contact" + } + ] + }, + "responses": { + "experience": { + "keywords": ["experiencia", "trabajo", "laboral", "empresa", "puesto", "carrera"], + "response": "Pablo tiene más de 5 años de experiencia como desarrollador Full Stack. Actualmente es Senior Developer en TechCorp Solutions, donde lidera un equipo de 5 desarrolladores y ha implementado arquitecturas de microservicios que redujeron los tiempos de deployment en un 80%. Anteriormente trabajó en StartupXYZ optimizando performance y en DevAgency desarrollando APIs REST." + }, + "skills": { + "keywords": ["habilidades", "tecnologías", "stack", "lenguajes", "frameworks"], + "response": "Pablo domina un amplio stack tecnológico: Frontend con Vue.js (95%), React (90%) y TypeScript (85%). En Backend maneja Node.js (95%), Express.js (90%) y Python (75%). También tiene experiencia en bases de datos como PostgreSQL y MongoDB, y en DevOps con Docker, AWS y Kubernetes." + }, + "projects": { + "keywords": ["proyectos", "desarrollado", "creado", "portfolio", "aplicaciones"], + "response": "Entre sus proyectos destacados están: una plataforma de e-commerce con +50,000 usuarios activos, un dashboard de analytics en tiempo real que procesa +1M eventos/día, y la migración de un monolito a arquitectura de microservicios que mejoró la disponibilidad al 99.95%." + }, + "contact": { + "keywords": ["contacto", "email", "teléfono", "linkedin", "ubicación"], + "response": "Puedes contactar a Pablo por email: pablo.delatorre@ejemplo.com, teléfono: +54 11 1234-5678. También está en LinkedIn: linkedin.com/in/pablo-delatorre y GitHub: github.com/pablo-delatorre. Se encuentra en Buenos Aires, Argentina." + }, + "education": { + "keywords": ["educación", "estudios", "universidad", "carrera", "certificaciones"], + "response": "Pablo es Ingeniero en Sistemas de Información por la UTN (2014-2018) con especialización en Desarrollo de Software. Tiene certificaciones de AWS Developer Associate, MongoDB Certified Developer y Certified Scrum Master." + } + }, + "fallback": [ + "Esa es una excelente pregunta. Pablo siempre busca mantenerse actualizado con las últimas tecnologías.", + "Interesante punto. En su experiencia, ha encontrado que la clave está en el equilibrio entre innovación y estabilidad.", + "Desde su perspectiva técnica, considera fundamental evaluar cada herramienta según el contexto del proyecto." + ] + } +} diff --git a/src/infrastructure/repositories/ChatRepository.js b/src/infrastructure/repositories/ChatRepository.js new file mode 100644 index 0000000..fdcec52 --- /dev/null +++ b/src/infrastructure/repositories/ChatRepository.js @@ -0,0 +1,34 @@ +export class ChatRepository { + constructor(chatConfig) { + this.chatConfig = chatConfig + } + + async getResponse(message) { + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 1000)) + + const lowerMessage = message.toLowerCase() + + // Search in configured responses + for (const [category, data] of Object.entries(this.chatConfig.responses)) { + if (data.keywords.some((keyword) => lowerMessage.includes(keyword))) { + return data.response + } + } + + // Return fallback response + const fallbackResponses = this.chatConfig.fallback || [ + "Esa es una excelente pregunta. ¿Hay algo específico que te gustaría saber?", + ] + + return fallbackResponses[Math.floor(Math.random() * fallbackResponses.length)] + } + + async getWelcomeMessage() { + return this.chatConfig.welcome?.message || "¡Hola! ¿En qué puedo ayudarte?" + } + + async getQuickActions() { + return this.chatConfig.welcome?.quickActions || [] + } +} diff --git a/src/infrastructure/repositories/PortfolioRepository.js b/src/infrastructure/repositories/PortfolioRepository.js new file mode 100644 index 0000000..bf82bd6 --- /dev/null +++ b/src/infrastructure/repositories/PortfolioRepository.js @@ -0,0 +1,40 @@ +import portfolioConfig from "@/data/portfolio-config.json" + +export class PortfolioRepository { + async getPortfolioData() { + // Simulate API call delay + await new Promise((resolve) => setTimeout(resolve, 100)) + + return portfolioConfig + } + + async getPersonalInfo() { + const data = await this.getPortfolioData() + return data.personal + } + + async getExperience() { + const data = await this.getPortfolioData() + return data.experience + } + + async getProjects() { + const data = await this.getPortfolioData() + return data.projects + } + + async getSkills() { + const data = await this.getPortfolioData() + return data.skills + } + + async getEducation() { + const data = await this.getPortfolioData() + return data.education + } + + async getCertifications() { + const data = await this.getPortfolioData() + return data.certifications + } +} diff --git a/src/services/ChatService.js b/src/services/ChatService.js new file mode 100644 index 0000000..e2e189a --- /dev/null +++ b/src/services/ChatService.js @@ -0,0 +1,77 @@ +import { ref } from "vue" +import { ChatRepository } from "@/infrastructure/repositories/ChatRepository" + +export function useChatService(chatConfig) { + const messages = ref([]) + const input = ref("") + const isLoading = ref(false) + + const chatRepository = new ChatRepository(chatConfig) + + async function sendMessage(text = null) { + const messageText = text || input.value.trim() + if (!messageText || isLoading.value) return + + // Add user message + const userMessage = { + id: Date.now(), + role: "user", + content: messageText, + timestamp: Date.now(), + } + messages.value.push(userMessage) + + // Clear input + input.value = "" + isLoading.value = true + + // Add typing indicator + const typingMessage = { + id: Date.now() + 1, + role: "assistant", + content: "", + typing: true, + } + messages.value.push(typingMessage) + + try { + // Get response from repository + const response = await chatRepository.getResponse(messageText) + + // Remove typing indicator + messages.value.pop() + + // Add assistant response + const assistantMessage = { + id: Date.now() + 2, + role: "assistant", + content: response, + timestamp: Date.now(), + } + messages.value.push(assistantMessage) + } catch (error) { + console.error("Error getting chat response:", error) + + // Remove typing indicator + messages.value.pop() + + // Add error message + const errorMessage = { + id: Date.now() + 2, + role: "assistant", + content: "Lo siento, ha ocurrido un error. Por favor, intenta de nuevo.", + timestamp: Date.now(), + } + messages.value.push(errorMessage) + } finally { + isLoading.value = false + } + } + + return { + messages, + input, + isLoading, + sendMessage, + } +} diff --git a/src/style.css b/src/style.css index 752bd43..3312515 100644 --- a/src/style.css +++ b/src/style.css @@ -52,12 +52,14 @@ } ::-webkit-scrollbar-thumb { - background: rgba(147, 51, 234, 0.5); + /*background: rgba(147, 51, 234, 0.5);*/ + background: rgba(59, 130, 246, 0.5); /* Blue-500 */ border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(147, 51, 234, 0.7); + /*background: rgba(147, 51, 234, 0.7);*7 + background: rgba(59, 130, 246, 0.7); /* Blue-500 */ } /* Animaciones personalizadas */ diff --git a/tailwind.config.js b/tailwind.config.js index 771e8e7..198de87 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -51,13 +51,13 @@ module.exports = { foreground: "hsl(var(--card-foreground))", }, purple: { - 400: "#a855f7", - 500: "#9333ea", - 600: "#7c3aed", + 400: "#475569", // slate-600 + 500: "#334155", // slate-700 + 600: "#1e293b", // slate-800 }, pink: { - 500: "#ec4899", - 600: "#db2777", + 500: "#0ea5e9", // sky-500 (azul vibrante, pero no chillón) + 600: "#0284c7", // sky-600 }, }, animation: { @@ -82,4 +82,4 @@ module.exports = { }, }, plugins: [], -}; \ No newline at end of file +};