Initial functional version of the portfolio chatbot site
This commit is contained in:
parent
9eca12ebca
commit
a28728af2a
6
app.vue
6
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 */
|
||||
}
|
||||
</style>
|
||||
|
122
package-lock.json
generated
122
package-lock.json
generated
@ -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": "*"
|
||||
|
215
src/App.vue
215
src/App.vue
@ -1,156 +1,103 @@
|
||||
<template>
|
||||
<div :class="{ 'dark': isDark }" class="min-h-screen transition-colors duration-300">
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 text-white">
|
||||
<!-- Header Component -->
|
||||
<AppHeader
|
||||
:isDark="isDark"
|
||||
@toggle-theme="toggleTheme"
|
||||
:isOnline="isOnline"
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
|
||||
<!-- Navigation -->
|
||||
<AppNavigation
|
||||
:sections="navigationSections"
|
||||
@navigate="scrollToSection"
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<HeroSection
|
||||
id="hero"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
|
||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<!-- Welcome Section -->
|
||||
<WelcomeSection
|
||||
v-if="messages.length === 0"
|
||||
:quickSuggestions="quickSuggestions"
|
||||
@send-message="sendMessage"
|
||||
/>
|
||||
<!-- About Section -->
|
||||
<AboutSection
|
||||
id="about"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<ChatMessages
|
||||
:messages="messages"
|
||||
:isLoading="isLoading"
|
||||
ref="chatMessages"
|
||||
/>
|
||||
<!-- Experience Section -->
|
||||
<ExperienceSection
|
||||
id="experience"
|
||||
:experience="portfolioData.experience"
|
||||
/>
|
||||
|
||||
<!-- Input Form -->
|
||||
<ChatInput
|
||||
:input="input"
|
||||
:isLoading="isLoading"
|
||||
@update:input="input = $event"
|
||||
@send-message="handleSubmit"
|
||||
/>
|
||||
<!-- Projects Section -->
|
||||
<ProjectsSection
|
||||
id="projects"
|
||||
:projects="portfolioData.projects"
|
||||
/>
|
||||
|
||||
<!-- Tech Stack Display -->
|
||||
<TechStack :techStack="techStack" />
|
||||
<!-- Skills Section -->
|
||||
<SkillsSection
|
||||
id="skills"
|
||||
:skills="portfolioData.skills"
|
||||
/>
|
||||
|
||||
<!-- Footer -->
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contact Section -->
|
||||
<ContactSection
|
||||
id="contact"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<AppFooter :personal="portfolioData.personal" />
|
||||
|
||||
<!-- Floating Chat Button -->
|
||||
<ChatFloatingButton @toggle-chat="toggleChat" />
|
||||
|
||||
<!-- Chat Popup -->
|
||||
<ChatPopup
|
||||
v-if="isChatOpen"
|
||||
:chatConfig="portfolioData.chatbot"
|
||||
@close="toggleChat"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import WelcomeSection from './components/WelcomeSection.vue'
|
||||
import ChatMessages from './components/ChatMessages.vue'
|
||||
import ChatInput from './components/ChatInput.vue'
|
||||
import TechStack from './components/TechStack.vue'
|
||||
import AppFooter from './components/AppFooter.vue'
|
||||
import { useKnowledgeBase } from './composables/useKnowledgeBase'
|
||||
import { useChat } from './composables/useChat'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { usePortfolioData } from '@/composables/usePortfolioData'
|
||||
import { useNavigation } from '@/composables/useNavigation'
|
||||
|
||||
const isDark = ref(true)
|
||||
const isOnline = ref(navigator.onLine)
|
||||
// Components
|
||||
import AppNavigation from '@/components/layout/AppNavigation.vue'
|
||||
import AppFooter from '@/components/layout/AppFooter.vue'
|
||||
import HeroSection from '@/components/sections/HeroSection.vue'
|
||||
import AboutSection from '@/components/sections/AboutSection.vue'
|
||||
import ExperienceSection from '@/components/sections/ExperienceSection.vue'
|
||||
import ProjectsSection from '@/components/sections/ProjectsSection.vue'
|
||||
import SkillsSection from '@/components/sections/SkillsSection.vue'
|
||||
import ContactSection from '@/components/sections/ContactSection.vue'
|
||||
import ChatFloatingButton from '@/components/chat/ChatFloatingButton.vue'
|
||||
import ChatPopup from '@/components/chat/ChatPopup.vue'
|
||||
|
||||
// Composables
|
||||
const { findBestResponse } = useKnowledgeBase()
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
isLoading,
|
||||
sendMessage: sendChatMessage,
|
||||
handleSubmit
|
||||
} = useChat(findBestResponse)
|
||||
const { portfolioData, loadPortfolioData } = usePortfolioData()
|
||||
const { scrollToSection } = useNavigation()
|
||||
|
||||
const chatMessages = ref(null)
|
||||
// State
|
||||
const isChatOpen = ref(false)
|
||||
|
||||
const techStack = [
|
||||
'Vue.js 3', 'React 18', 'Node.js', 'TypeScript', 'Python', 'Docker',
|
||||
'AWS', 'MongoDB', 'PostgreSQL', 'Git', 'CI/CD', 'Microservicios',
|
||||
'Tailwind CSS', 'Express.js', 'FastAPI', 'Redis', 'Kubernetes'
|
||||
const navigationSections = [
|
||||
{ id: 'hero', label: 'Inicio' },
|
||||
{ id: 'about', label: 'Sobre mí' },
|
||||
{ id: 'experience', label: 'Experiencia' },
|
||||
{ id: 'projects', label: 'Proyectos' },
|
||||
{ id: 'skills', label: 'Habilidades' },
|
||||
{ id: 'contact', label: 'Contacto' }
|
||||
]
|
||||
|
||||
const quickSuggestions = [
|
||||
{
|
||||
icon: 'Briefcase',
|
||||
title: 'Experiencia',
|
||||
text: '¿Cuál es tu experiencia laboral?'
|
||||
},
|
||||
{
|
||||
icon: 'Code',
|
||||
title: 'Habilidades',
|
||||
text: '¿Qué tecnologías dominas?'
|
||||
},
|
||||
{
|
||||
icon: 'Rocket',
|
||||
title: 'Proyectos',
|
||||
text: 'Cuéntame sobre tus proyectos destacados'
|
||||
},
|
||||
{
|
||||
icon: 'GraduationCap',
|
||||
title: 'Educación',
|
||||
text: '¿Cuál es tu formación académica?'
|
||||
},
|
||||
{
|
||||
icon: 'Mail',
|
||||
title: 'Contacto',
|
||||
text: '¿Cómo puedo contactarte?'
|
||||
},
|
||||
{
|
||||
icon: 'DollarSign',
|
||||
title: 'Salario',
|
||||
text: '¿Cuáles son tus expectativas salariales?'
|
||||
}
|
||||
]
|
||||
|
||||
function sendMessage(text) {
|
||||
sendChatMessage(text)
|
||||
nextTick(() => {
|
||||
if (chatMessages.value) {
|
||||
chatMessages.value.scrollToBottom()
|
||||
}
|
||||
})
|
||||
function toggleChat() {
|
||||
isChatOpen.value = !isChatOpen.value
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
onMounted(() => {
|
||||
// Cargar tema guardado
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
if (savedTheme) {
|
||||
isDark.value = savedTheme === 'dark'
|
||||
}
|
||||
|
||||
// Listener para estado de conexión
|
||||
window.addEventListener('online', () => isOnline.value = true)
|
||||
window.addEventListener('offline', () => isOnline.value = false)
|
||||
|
||||
// Mensaje de bienvenida
|
||||
setTimeout(() => {
|
||||
const welcomeMessage = {
|
||||
id: Date.now(),
|
||||
role: 'assistant',
|
||||
content: `
|
||||
¡Hola! 👋 Soy tu asistente de portfolio inteligente.<br><br>
|
||||
|
||||
Puedo contarte sobre:<br>
|
||||
• 💼 Mi experiencia profesional<br>
|
||||
• 🚀 Habilidades técnicas<br>
|
||||
• 🎯 Proyectos destacados<br>
|
||||
• 🎓 Formación académica<br>
|
||||
• 📞 Información de contacto<br><br>
|
||||
|
||||
<em>¿Qué te gustaría saber?</em>
|
||||
`
|
||||
}
|
||||
//messages.value.push(welcomeMessage)
|
||||
}, 1000)
|
||||
onMounted(async () => {
|
||||
await loadPortfolioData()
|
||||
})
|
||||
</script>
|
||||
|
21
src/components/chat/ChatFloatingButton.vue
Normal file
21
src/components/chat/ChatFloatingButton.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="fixed bottom-6 right-6 z-50">
|
||||
<button
|
||||
@click="$emit('toggle-chat')"
|
||||
class="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110 flex items-center justify-center"
|
||||
>
|
||||
<MessageCircle class="w-8 h-8" />
|
||||
</button>
|
||||
|
||||
<!-- Notification Badge -->
|
||||
<div class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs rounded-full flex items-center justify-center animate-pulse">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MessageCircle } from 'lucide-vue-next'
|
||||
|
||||
defineEmits(['toggle-chat'])
|
||||
</script>
|
148
src/components/chat/ChatPopup.vue
Normal file
148
src/components/chat/ChatPopup.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 z-50 flex items-end justify-end p-4">
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
||||
@click="$emit('close')"
|
||||
></div>
|
||||
|
||||
<!-- Chat Window -->
|
||||
<div class="relative w-full max-w-md h-96 bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img
|
||||
:src="avatarBot"
|
||||
alt="Assistant"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Asistente Virtual</h3>
|
||||
<p class="text-xs text-green-500">En línea</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" ref="messagesContainer">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="flex items-start space-x-2"
|
||||
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
||||
>
|
||||
<img
|
||||
:src="message.role === 'user' ? avatarUser : avatarBot"
|
||||
alt="avatar"
|
||||
class="w-6 h-6 rounded-full flex-shrink-0"
|
||||
/>
|
||||
<div
|
||||
class="max-w-xs px-3 py-2 rounded-lg text-sm"
|
||||
:class="message.role === 'user'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'"
|
||||
>
|
||||
<div v-if="message.typing" class="flex space-x-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
</div>
|
||||
<div v-else v-html="message.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div v-if="showQuickActions" class="px-4 pb-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="action in chatConfig.welcome.quickActions"
|
||||
:key="action.text"
|
||||
@click="sendMessage(action.text)"
|
||||
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-xs hover:bg-purple-200 dark:hover:bg-purple-800 transition-colors"
|
||||
>
|
||||
{{ action.text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="input"
|
||||
@keydown.enter="handleSubmit"
|
||||
:disabled="isLoading"
|
||||
placeholder="Escribe tu pregunta..."
|
||||
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
|
||||
/>
|
||||
<button
|
||||
@click="handleSubmit"
|
||||
:disabled="isLoading || !input.trim()"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Send class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||||
import { X, Send } from 'lucide-vue-next'
|
||||
import { useChatService } from '@/services/ChatService'
|
||||
import avatarUser from '@/assets/avatar-user.jpg'
|
||||
import avatarBot from '@/assets/avatar-bot.png'
|
||||
|
||||
const props = defineProps({
|
||||
chatConfig: Object
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const { messages, input, isLoading, sendMessage: sendChatMessage } = useChatService(props.chatConfig)
|
||||
const messagesContainer = ref(null)
|
||||
|
||||
const showQuickActions = computed(() => {
|
||||
return messages.value.length <= 1 && props.chatConfig?.welcome?.quickActions
|
||||
})
|
||||
|
||||
function sendMessage(text) {
|
||||
sendChatMessage(text)
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (input.value.trim()) {
|
||||
sendMessage(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Send welcome message
|
||||
if (props.chatConfig?.welcome?.message) {
|
||||
setTimeout(() => {
|
||||
messages.value.push({
|
||||
id: Date.now(),
|
||||
role: 'assistant',
|
||||
content: props.chatConfig.welcome.message
|
||||
})
|
||||
nextTick(() => scrollToBottom())
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
74
src/components/layout/AppFooter.vue
Normal file
74
src/components/layout/AppFooter.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<footer class="bg-gray-900 text-white py-12">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
<!-- About -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">{{ personal.name }}</h3>
|
||||
<p class="text-gray-300 mb-4">
|
||||
{{ personal.title }} especializado en crear experiencias web excepcionales.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-5 h-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Enlaces Rápidos</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#about" class="text-gray-300 hover:text-white transition-colors">Sobre mí</a></li>
|
||||
<li><a href="#experience" class="text-gray-300 hover:text-white transition-colors">Experiencia</a></li>
|
||||
<li><a href="#projects" class="text-gray-300 hover:text-white transition-colors">Proyectos</a></li>
|
||||
<li><a href="#skills" class="text-gray-300 hover:text-white transition-colors">Habilidades</a></li>
|
||||
<li><a href="#contact" class="text-gray-300 hover:text-white transition-colors">Contacto</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Contacto</h3>
|
||||
<div class="space-y-2 text-gray-300">
|
||||
<p>{{ personal.email }}</p>
|
||||
<p>{{ personal.phone }}</p>
|
||||
<p>{{ personal.location }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© {{ currentYear }} {{ personal.name }}. Todos los derechos reservados.</p>
|
||||
<p class="mt-2 text-sm">Hecho con ❤️ y Vue.js</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
</script>
|
72
src/components/layout/AppNavigation.vue
Normal file
72
src/components/layout/AppNavigation.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<nav class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-bold text-sm">P</span>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900 dark:text-white">Pablo de la Torre Jamardo</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="$emit('navigate', section.id)"
|
||||
class="text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 transition-colors font-medium"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
@click="toggleMobileMenu"
|
||||
class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<Menu v-if="!isMobileMenuOpen" class="w-6 h-6" />
|
||||
<X v-else class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div v-if="isMobileMenuOpen" class="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="handleMobileNavigate(section.id)"
|
||||
class="text-left px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Menu, X } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
sections: Array
|
||||
})
|
||||
|
||||
const emit = defineEmits(['navigate'])
|
||||
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
function handleMobileNavigate(sectionId) {
|
||||
emit('navigate', sectionId)
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
65
src/components/sections/AboutSection.vue
Normal file
65
src/components/sections/AboutSection.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<section id="about" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Sobre mí
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
||||
<!-- Bio -->
|
||||
<div>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||
{{ personal.bio }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<MapPin class="w-5 h-5 text-purple-600" />
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ personal.location }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Mail class="w-5 h-5 text-purple-600" />
|
||||
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Phone class="w-5 h-5 text-purple-600" />
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ personal.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">5+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Años de Experiencia</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">50+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Proyectos Completados</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">15+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Tecnologías</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">100%</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Satisfacción</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MapPin, Mail, Phone } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
</script>
|
170
src/components/sections/ContactSection.vue
Normal file
170
src/components/sections/ContactSection.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<section id="contact" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Contacto
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-12">
|
||||
<!-- Contact Info -->
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
¡Hablemos!
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Estoy siempre abierto a discutir nuevas oportunidades, proyectos interesantes o simplemente charlar sobre tecnología.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<Mail class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Email</div>
|
||||
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<Phone class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Teléfono</div>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ personal.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<MapPin class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Ubicación</div>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ personal.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="w-12 h-12 bg-gray-100 dark:bg-gray-700 hover:bg-purple-100 dark:hover:bg-purple-900 rounded-full flex items-center justify-center transition-colors"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl p-8">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea
|
||||
v-model="form.message"
|
||||
rows="4"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
{{ isSubmitting ? 'Enviando...' : 'Enviar Mensaje' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Mail, Phone, MapPin, Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log('Form submitted:', form.value)
|
||||
|
||||
// Reset form
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
isSubmitting.value = false
|
||||
|
||||
// Show success message (you could use a toast notification)
|
||||
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
|
||||
}
|
||||
</script>
|
92
src/components/sections/ExperienceSection.vue
Normal file
92
src/components/sections/ExperienceSection.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<section id="experience" class="py-20 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Experiencia Profesional
|
||||
</h2>
|
||||
|
||||
<div class="relative">
|
||||
<!-- Timeline Line -->
|
||||
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-purple-200 dark:bg-purple-800"></div>
|
||||
|
||||
<!-- Experience Items -->
|
||||
<div class="space-y-12">
|
||||
<div
|
||||
v-for="(exp, index) in experience"
|
||||
:key="exp.id"
|
||||
class="relative flex items-start space-x-6"
|
||||
>
|
||||
<!-- Timeline Dot -->
|
||||
<div class="flex-shrink-0 w-16 h-16 bg-purple-600 rounded-full flex items-center justify-center relative z-10">
|
||||
<Briefcase class="w-8 h-8 text-white" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{{ exp.position }}
|
||||
</h3>
|
||||
<h4 class="text-lg text-purple-600 dark:text-purple-400 font-semibold">
|
||||
{{ exp.company }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mt-2 md:mt-0">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Calendar class="w-4 h-4" />
|
||||
<span>{{ exp.period }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mt-1">
|
||||
<MapPin class="w-4 h-4" />
|
||||
<span>{{ exp.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{{ exp.description }}
|
||||
</p>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tech in exp.technologies"
|
||||
:key="tech"
|
||||
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Achievements -->
|
||||
<div class="space-y-2">
|
||||
<h5 class="font-semibold text-gray-900 dark:text-white">Logros destacados:</h5>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="achievement in exp.achievements"
|
||||
:key="achievement"
|
||||
class="flex items-start space-x-2 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<CheckCircle class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{{ achievement }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Briefcase, Calendar, MapPin, CheckCircle } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
experience: Array
|
||||
})
|
||||
</script>
|
85
src/components/sections/HeroSection.vue
Normal file
85
src/components/sections/HeroSection.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<section class="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-900 dark:to-purple-900 pt-16">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Avatar -->
|
||||
<div class="mb-8">
|
||||
<img
|
||||
:src="personal.avatar"
|
||||
:alt="personal.name"
|
||||
class="w-32 h-32 rounded-full mx-auto shadow-2xl border-4 border-white dark:border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name and Title -->
|
||||
<h1 class="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ personal.name }}
|
||||
</h1>
|
||||
|
||||
<h2 class="text-2xl md:text-3xl text-purple-600 dark:text-purple-400 font-semibold mb-6">
|
||||
{{ personal.title }}
|
||||
</h2>
|
||||
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
|
||||
{{ personal.subtitle }}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
||||
<button
|
||||
@click="scrollToSection('projects')"
|
||||
class="px-8 py-4 bg-purple-600 hover:bg-purple-700 text-white rounded-full font-semibold transition-all transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Ver Proyectos
|
||||
</button>
|
||||
<button
|
||||
@click="scrollToSection('contact')"
|
||||
class="px-8 py-4 border-2 border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-600 hover:text-white rounded-full font-semibold transition-all"
|
||||
>
|
||||
Contactar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="flex justify-center space-x-6">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<ChevronDown class="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Github, Linkedin, Twitter, Globe, ChevronDown } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
function scrollToSection(sectionId) {
|
||||
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
99
src/components/sections/ProjectsSection.vue
Normal file
99
src/components/sections/ProjectsSection.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<section id="projects" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Proyectos Destacados
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
class="bg-gray-50 dark:bg-gray-700 rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all transform hover:scale-105"
|
||||
>
|
||||
<!-- Project Image -->
|
||||
<div class="relative h-48 overflow-hidden">
|
||||
<img
|
||||
:src="project.image"
|
||||
:alt="project.title"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Project Content -->
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ project.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4 line-clamp-3">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tech in project.technologies.slice(0, 3)"
|
||||
:key="tech"
|
||||
class="px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
<span
|
||||
v-if="project.technologies.length > 3"
|
||||
class="px-2 py-1 bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded text-xs"
|
||||
>
|
||||
+{{ project.technologies.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
|
||||
<div
|
||||
v-for="(value, key) in project.metrics"
|
||||
:key="key"
|
||||
class="text-center p-2 bg-white dark:bg-gray-600 rounded"
|
||||
>
|
||||
<div class="font-bold text-purple-600 dark:text-purple-400">{{ value }}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 capitalize">{{ key }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="flex space-x-2">
|
||||
<a
|
||||
v-if="project.links.demo"
|
||||
:href="project.links.demo"
|
||||
target="_blank"
|
||||
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink class="w-4 h-4 inline mr-1" />
|
||||
Demo
|
||||
</a>
|
||||
<a
|
||||
v-if="project.links.github"
|
||||
:href="project.links.github"
|
||||
target="_blank"
|
||||
class="flex-1 px-4 py-2 border border-purple-600 text-purple-600 hover:bg-purple-600 hover:text-white rounded-lg text-center text-sm font-medium transition-colors"
|
||||
>
|
||||
<Github class="w-4 h-4 inline mr-1" />
|
||||
Código
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ExternalLink, Github } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
projects: Array
|
||||
})
|
||||
</script>
|
75
src/components/sections/SkillsSection.vue
Normal file
75
src/components/sections/SkillsSection.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<section id="skills" class="py-20 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Habilidades Técnicas
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div
|
||||
v-for="(skillGroup, category) in skills"
|
||||
:key="category"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg"
|
||||
>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6 capitalize flex items-center">
|
||||
<component :is="getCategoryIcon(category)" class="w-6 h-6 mr-2 text-purple-600" />
|
||||
{{ getCategoryName(category) }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="skill in skillGroup"
|
||||
:key="skill.name"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ skill.name }}</span>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>{{ skill.years }} años</span>
|
||||
<span>{{ skill.level }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-1000"
|
||||
:style="{ width: `${skill.level}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Code, Server, Database, Cloud } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
skills: Object
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
const icons = {
|
||||
frontend: Code,
|
||||
backend: Server,
|
||||
database: Database,
|
||||
devops: Cloud
|
||||
}
|
||||
return icons[category] || Code
|
||||
}
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
frontend: 'Frontend',
|
||||
backend: 'Backend',
|
||||
database: 'Base de Datos',
|
||||
devops: 'DevOps'
|
||||
}
|
||||
return names[category] || category
|
||||
}
|
||||
</script>
|
18
src/composables/useNavigation.js
Normal file
18
src/composables/useNavigation.js
Normal file
@ -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,
|
||||
}
|
||||
}
|
30
src/composables/usePortfolioData.js
Normal file
30
src/composables/usePortfolioData.js
Normal file
@ -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,
|
||||
}
|
||||
}
|
228
src/data/portfolio-config.json
Normal file
228
src/data/portfolio-config.json
Normal file
@ -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."
|
||||
]
|
||||
}
|
||||
}
|
34
src/infrastructure/repositories/ChatRepository.js
Normal file
34
src/infrastructure/repositories/ChatRepository.js
Normal file
@ -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 || []
|
||||
}
|
||||
}
|
40
src/infrastructure/repositories/PortfolioRepository.js
Normal file
40
src/infrastructure/repositories/PortfolioRepository.js
Normal file
@ -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
|
||||
}
|
||||
}
|
77
src/services/ChatService.js
Normal file
77
src/services/ChatService.js
Normal file
@ -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,
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
@ -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: [],
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user