Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

64 changed files with 1530 additions and 2487 deletions

1
.gitignore vendored
View File

@ -43,4 +43,3 @@ Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
Icon?

View File

@ -77,9 +77,9 @@ npm run deploy
## ⚙️ Personalización
### 1. Información Profile
### 1. Información Personal
Edita `src/composables/useKnowledgeBase.ts`:
Edita `src/composables/useKnowledgeBase.js`:
\`\`\`javascript
const knowledgeBase = ref({
@ -148,10 +148,10 @@ ai-portfolio-chat/
│ │ ├── TechStack.vue
│ │ └── AppFooter.vue
│ ├── composables/ # Lógica reutilizable
│ ├── useKnowledgeBase.ts
│ └── useChat.ts
│ ├── useKnowledgeBase.js
│ └── useChat.js
│ ├── App.vue # Componente principal
├── main.ts # Punto de entrada
├── main.js # Punto de entrada
│ └── style.css # Estilos globales
├── public/ # Archivos estáticos
├── .github/workflows/ # GitHub Actions

473
app.vue Normal file
View File

@ -0,0 +1,473 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 text-white">
<!-- Header -->
<header class="border-b border-white/10 backdrop-blur-sm bg-black/20">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
<span class="text-lg font-bold">AI</span>
</div>
<div>
<h1 class="text-xl font-bold">Portfolio Assistant</h1>
<p class="text-sm text-gray-300">Powered by Advanced AI</p>
</div>
</div>
<button
@click="toggleTheme"
class="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
>
<component :is="isDark ? 'Sun' : 'Moon'" class="w-5 h-5" />
</button>
</div>
</header>
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Welcome Section -->
<div v-if="messages.length === 0" class="text-center mb-8">
<div class="mb-6">
<div class="w-24 h-24 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full mx-auto mb-4 flex items-center justify-center">
<Bot class="w-12 h-12" />
</div>
<h2 class="text-3xl font-bold mb-2">¡Hola! Soy tu Asistente de Portfolio</h2>
<p class="text-gray-300 text-lg">Pregúntame sobre experiencia, habilidades, proyectos o cualquier cosa técnica</p>
</div>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<button
v-for="suggestion in quickSuggestions"
:key="suggestion.text"
@click="sendMessage(suggestion.text)"
class="p-4 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 transition-all hover:scale-105 text-left"
>
<component :is="suggestion.icon" class="w-6 h-6 mb-2 text-purple-400" />
<h3 class="font-semibold mb-1">{{ suggestion.title }}</h3>
<p class="text-sm text-gray-400">{{ suggestion.text }}</p>
</button>
</div>
</div>
<!-- Chat Messages -->
<div class="space-y-4 mb-6" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
class="flex items-start space-x-3"
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
>
<div class="flex-shrink-0">
<div
class="w-8 h-8 rounded-full flex items-center justify-center"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500'
: 'bg-gradient-to-r from-purple-500 to-pink-500'"
>
<component :is="message.role === 'user' ? 'User' : 'Bot'" class="w-4 h-4" />
</div>
</div>
<div
class="max-w-xs lg:max-w-md px-4 py-2 rounded-2xl"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white'
: 'bg-white/10 backdrop-blur-sm border border-white/20'"
>
<div v-if="message.role === 'assistant' && message.typing" class="flex space-x-1">
<div class="w-2 h-2 bg-purple-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<div v-else v-html="formatMessage(message.content)"></div>
</div>
</div>
</div>
<!-- Input Form -->
<form @submit.prevent="handleSubmit" class="relative">
<div class="flex space-x-2">
<input
v-model="input"
:disabled="isLoading"
placeholder="Pregúntame sobre mi experiencia, habilidades, proyectos..."
class="flex-1 px-4 py-3 bg-white/10 backdrop-blur-sm border border-white/20 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder-gray-400"
/>
<button
type="submit"
:disabled="isLoading || !input.trim()"
class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:from-purple-600 hover:to-pink-600 transition-all"
>
<Send class="w-5 h-5" />
</button>
</div>
</form>
<!-- Tech Stack Display -->
<div class="mt-8 p-6 bg-white/5 backdrop-blur-sm rounded-xl border border-white/10">
<h3 class="text-lg font-semibold mb-4 flex items-center">
<Code class="w-5 h-5 mr-2 text-purple-400" />
Stack Tecnológico Principal
</h3>
<div class="flex flex-wrap gap-2">
<span v-for="tech in techStack" :key="tech"
class="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm border border-purple-500/30">
{{ tech }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, nextTick, onMounted } from 'vue'
import { Bot, User, Send, Sun, Moon, Code, Briefcase, Award, Rocket } from 'lucide-vue-next'
const messages = ref([])
const input = ref('')
const isLoading = ref(false)
const isDark = ref(true)
const messagesContainer = ref(null)
const techStack = [
'Vue.js', 'React', 'Node.js', 'TypeScript', 'Python', 'Docker',
'AWS', 'MongoDB', 'PostgreSQL', 'Git', 'CI/CD', 'Microservicios'
]
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'
}
]
// Base de conocimiento pre-programada
const knowledgeBase = {
experiencia: {
keywords: ['experiencia', 'trabajo', 'laboral', 'años', 'empresa', 'puesto'],
response: `
<strong>💼 Experiencia Profesional</strong><br><br>
<strong>Senior Full Stack Developer</strong> (2021 - Presente)<br>
Liderazgo de equipo de 5 desarrolladores<br>
Arquitectura de microservicios con Node.js y Docker<br>
Implementación de CI/CD reduciendo deploys en 80%<br><br>
<strong>Frontend Developer</strong> (2019 - 2021)<br>
Desarrollo de SPAs con Vue.js y React<br>
Optimización de performance (Core Web Vitals)<br>
Colaboración con equipos UX/UI<br><br>
<strong>Junior Developer</strong> (2018 - 2019)<br>
Desarrollo de APIs REST con Express.js<br>
Integración con bases de datos SQL y NoSQL<br>
Metodologías ágiles (Scrum/Kanban)
`
},
habilidades: {
keywords: ['habilidades', 'tecnologías', 'stack', 'lenguajes', 'frameworks', 'dominas'],
response: `
<strong>🚀 Stack Tecnológico</strong><br><br>
<strong>Frontend:</strong><br>
Vue.js 3 (Composition API, Pinia) - Avanzado<br>
React (Hooks, Context, Redux) - Avanzado<br>
TypeScript - Avanzado<br>
Tailwind CSS, SCSS - Avanzado<br><br>
<strong>Backend:</strong><br>
Node.js (Express, Fastify) - Avanzado<br>
Python (Django, FastAPI) - Intermedio<br>
Bases de datos: PostgreSQL, MongoDB - Avanzado<br><br>
<strong>DevOps & Cloud:</strong><br>
Docker, Kubernetes - Intermedio<br>
AWS (EC2, S3, Lambda) - Intermedio<br>
CI/CD (GitHub Actions, Jenkins) - Avanzado
`
},
proyectos: {
keywords: ['proyectos', 'desarrollado', 'creado', 'portfolio', 'destacados'],
response: `
<strong>🎯 Proyectos Destacados</strong><br><br>
<strong>E-commerce Platform</strong><br>
Plataforma completa con Vue.js + Node.js<br>
+50,000 usuarios activos mensuales<br>
Integración con Stripe y PayPal<br>
<em>Tech:</em> Vue 3, Express, PostgreSQL, Redis<br><br>
<strong>Real-time Analytics Dashboard</strong><br>
Dashboard en tiempo real con WebSockets<br>
Procesamiento de +1M eventos/día<br>
Visualizaciones interactivas con D3.js<br>
<em>Tech:</em> React, Socket.io, InfluxDB<br><br>
<strong>Microservices Architecture</strong><br>
Migración de monolito a microservicios<br>
Reducción de latencia en 60%<br>
Implementación con Docker y Kubernetes<br>
<em>Tech:</em> Node.js, Docker, AWS EKS
`
},
educacion: {
keywords: ['educación', 'estudios', 'universidad', 'carrera', 'certificaciones'],
response: `
<strong>🎓 Formación Académica</strong><br><br>
<strong>Ingeniería en Sistemas</strong><br>
Universidad Tecnológica Nacional (2014-2018)<br>
Especialización en Desarrollo de Software<br>
Proyecto final: Sistema de gestión hospitalaria<br><br>
<strong>Certificaciones:</strong><br>
AWS Certified Developer Associate (2022)<br>
MongoDB Certified Developer (2021)<br>
Scrum Master Certified (2020)<br><br>
<strong>Formación Continua:</strong><br>
Cursos especializados en arquitectura de software<br>
Participación en conferencias tech (JSConf, VueConf)<br>
Contribuciones a proyectos open source
`
},
contacto: {
keywords: ['contacto', 'email', 'linkedin', 'github', 'cv'],
response: `
<strong>📞 Información de Contacto</strong><br><br>
<strong>Email:</strong> tu.email@ejemplo.com<br>
<strong>LinkedIn:</strong> linkedin.com/in/tu-perfil<br>
<strong>GitHub:</strong> github.com/tu-usuario<br>
<strong>Portfolio:</strong> tu-portfolio.com<br><br>
<strong>Disponibilidad:</strong><br>
Disponible para nuevas oportunidades<br>
Modalidad: Remoto/Híbrido/Presencial<br>
Ubicación: Ciudad, País<br><br>
<em>¡No dudes en contactarme para discutir oportunidades!</em>
`
},
salario: {
keywords: ['salario', 'sueldo', 'pretensiones', 'económicas', 'remuneración'],
response: `
<strong>💰 Expectativas Salariales</strong><br><br>
Mis expectativas salariales son competitivas y están alineadas con:<br><br>
Mi experiencia de +5 años en desarrollo<br>
El mercado actual para Senior Developers<br>
La complejidad y responsabilidades del rol<br>
Los beneficios adicionales ofrecidos<br><br>
<em>Estoy abierto a discutir una propuesta integral que incluya salario base, beneficios y oportunidades de crecimiento.</em><br><br>
<strong>Factores importantes para :</strong><br>
Crecimiento profesional<br>
Ambiente de trabajo colaborativo<br>
Flexibilidad horaria<br>
Proyectos desafiantes
`
}
}
const defaultResponses = [
"Esa es una excelente pregunta. Como desarrollador senior, siempre busco mantenerme actualizado con las últimas tecnologías y mejores prácticas.",
"Interesante punto. En mi experiencia, he encontrado que la clave está en encontrar el equilibrio entre innovación y estabilidad.",
"Desde mi perspectiva técnica, considero que es fundamental evaluar cada herramienta en función del contexto específico del proyecto.",
"Basándome en mi experiencia en proyectos enterprise, puedo decir que la escalabilidad y mantenibilidad son aspectos cruciales.",
"Como alguien que ha trabajado tanto en startups como en empresas grandes, he aprendido a adaptar mi enfoque según las necesidades del negocio."
]
function findBestResponse(message) {
const lowerMessage = message.toLowerCase()
for (const [category, data] of Object.entries(knowledgeBase)) {
if (data.keywords.some(keyword => lowerMessage.includes(keyword))) {
return data.response
}
}
// Respuestas contextuales adicionales
if (lowerMessage.includes('react') || lowerMessage.includes('vue')) {
return `
<strong> React vs Vue.js</strong><br><br>
Tengo experiencia sólida con ambos frameworks:<br><br>
<strong>React:</strong><br>
Excelente ecosistema y comunidad<br>
Hooks y Context API para gestión de estado<br>
Ideal para aplicaciones complejas<br><br>
<strong>Vue.js:</strong><br>
Curva de aprendizaje más suave<br>
Composition API muy potente<br>
Excelente para desarrollo rápido<br><br>
<em>La elección depende del proyecto, equipo y requisitos específicos.</em>
`
}
if (lowerMessage.includes('node') || lowerMessage.includes('backend')) {
return `
<strong>🔧 Desarrollo Backend</strong><br><br>
Mi experiencia en backend incluye:<br><br>
<strong>Node.js:</strong> Express, Fastify, NestJS<br>
<strong>APIs:</strong> REST, GraphQL, WebSockets<br>
<strong>Bases de datos:</strong> PostgreSQL, MongoDB, Redis<br>
<strong>Arquitectura:</strong> Microservicios, Event-driven<br>
<strong>Testing:</strong> Jest, Mocha, Supertest<br><br>
<em>Siempre enfocado en código limpio, escalable y bien documentado.</em>
`
}
// Respuesta por defecto
return defaultResponses[Math.floor(Math.random() * defaultResponses.length)]
}
function formatMessage(content) {
return content.replace(/\n/g, '<br>')
}
async function sendMessage(text = null) {
const messageText = text || input.value.trim()
if (!messageText) return
// Agregar mensaje del usuario
const userMessage = {
id: Date.now(),
role: 'user',
content: messageText
}
messages.value.push(userMessage)
// Limpiar input
input.value = ''
isLoading.value = true
// Scroll to bottom
await nextTick()
scrollToBottom()
// Agregar mensaje de typing
const typingMessage = {
id: Date.now() + 1,
role: 'assistant',
content: '',
typing: true
}
messages.value.push(typingMessage)
await nextTick()
scrollToBottom()
// Simular delay de respuesta
setTimeout(() => {
// Remover mensaje de typing
messages.value.pop()
// Agregar respuesta real
const response = findBestResponse(messageText)
const assistantMessage = {
id: Date.now() + 2,
role: 'assistant',
content: response
}
messages.value.push(assistantMessage)
isLoading.value = false
nextTick(() => scrollToBottom())
}, 1500 + Math.random() * 1000) // Delay variable para mayor realismo
}
function handleSubmit() {
sendMessage()
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
function toggleTheme() {
isDark.value = !isDark.value
}
onMounted(() => {
// Mensaje de bienvenida después de un momento
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)
nextTick(() => scrollToBottom())
}, 1000)
})
</script>
<style scoped>
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
}
.animate-bounce {
animation: bounce 1s infinite;
}
/* Scrollbar personalizado */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(147, 51, 234, 0.5);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(147, 51, 234, 0.7);
}
</style>

View File

@ -1,41 +1,31 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Portfolio tecnológico interactivo con asistente de IA - Desarrollador Full Stack especializado en Vue.js, React y Node.js" />
<meta name="keywords" content="desarrollador, full stack, vue.js, react, node.js, portfolio, programador" />
<meta name="author" content="Tu Nombre" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://tu-usuario.github.io/ai-portfolio-chat/" />
<meta property="og:title" content="Portfolio AI Chat - Desarrollador Full Stack" />
<meta property="og:description" content="Portfolio tecnológico interactivo con asistente de IA" />
<meta property="og:image" content="/og-image.jpg" />
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Portfolio | Pablo de la Torre Jamardo (Pablo TJ)</title>
<!-- SEO -->
<meta name="description" content="Portfolio personal de Pablo de la Torre Jamardo (Pablo TJ): proyectos, desarrollo software, programación y soluciones tecnológicas." />
<meta name="keywords" content="portfolio, Pablo TJ, Pablo de la Torre, desarrollador, programación, software, tecnología" />
<meta name="robots" content="index, follow" />
<meta name="author" content="Pablo de la Torre Jamardo" />
<link rel="canonical" href="https://pablotj.com/" />
<!-- PWA / Mobile -->
<meta name="theme-color" content="#062342" />
<meta name="apple-mobile-web-app-title" content="Pablo TJ" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!-- Favicon & App Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
<link rel="manifest" href="/images/site.webmanifest" />
<link rel="shortcut icon" href="/images/favicon.ico" />
<!-- Open Graph (Facebook, LinkedIn, WhatsApp) -->
<meta property="og:title" content="Portfolio | Pablo de la Torre Jamardo (Pablo TJ)" />
<meta property="og:description" content="Explora el portfolio de Pablo TJ: proyectos de software, programación y desarrollo tecnológico." />
<meta property="og:image" content="https://pablotj.com/images/favicon.svg" />
<meta property="og:url" content="https://pablotj.com/" />
<meta property="og:type" content="website" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://tu-usuario.github.io/ai-portfolio-chat/" />
<meta property="twitter:title" content="Portfolio AI Chat - Desarrollador Full Stack" />
<meta property="twitter:description" content="Portfolio tecnológico interactivo con asistente de IA" />
<meta property="twitter:image" content="/og-image.jpg" />
<title>Portfolio AI Chat - Desarrollador Full Stack</title>
</head>
<body>
<div id="app"></div>
<script src="/src/main.ts" type="module"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

190
package-lock.json generated
View File

@ -9,12 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"vue": "^3.4.21",
"vue-loading-overlay": "^6.0.6",
"vue-preloader": "^1.1.4",
"vue-toastification": "^2.0.0-rc.5",
"vue-typer": "^1.2.0",
"vue3-typer": "^1.0.0"
"vue": "^3.4.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
@ -77,9 +72,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@ -887,39 +882,39 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
"integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==",
"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==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.0",
"@vue/shared": "3.5.18",
"@babel/parser": "^7.27.5",
"@vue/shared": "3.5.17",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"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==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
"integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/compiler-core": "3.5.17",
"@vue/shared": "3.5.17"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz",
"integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
"integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
"license": "MIT",
"dependencies": {
"@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",
"@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",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.17",
"postcss": "^8.5.6",
@ -927,63 +922,63 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz",
"integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
"integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/compiler-dom": "3.5.17",
"@vue/shared": "3.5.17"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
"integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz",
"integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.18"
"@vue/shared": "3.5.17"
}
},
"node_modules/@vue/runtime-core": {
"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==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
"integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/reactivity": "3.5.17",
"@vue/shared": "3.5.17"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz",
"integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
"integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.18",
"@vue/runtime-core": "3.5.18",
"@vue/shared": "3.5.18",
"@vue/reactivity": "3.5.17",
"@vue/runtime-core": "3.5.17",
"@vue/shared": "3.5.17",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz",
"integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
"integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/compiler-ssr": "3.5.17",
"@vue/shared": "3.5.17"
},
"peerDependencies": {
"vue": "3.5.18"
"vue": "3.5.17"
}
},
"node_modules/@vue/shared": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz",
"integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
"license": "MIT"
},
"node_modules/ansi-regex": {
@ -1333,9 +1328,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.191",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz",
"integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==",
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
"dev": true,
"license": "ISC"
},
@ -1860,12 +1855,6 @@
"node": ">=8"
}
},
"node_modules/lodash.split": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.split/-/lodash.split-4.4.2.tgz",
"integrity": "sha512-kn1IDX0aHfg0FsnPIyxCHTamZXt3YK3aExRH1LW8YhzP6+sCldTm8+E4aIg+nSmM6R4eqdWGrXWtfYI961bwIw==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@ -2920,16 +2909,16 @@
}
},
"node_modules/vue": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
"integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
"license": "MIT",
"dependencies": {
"@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"
"@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"
},
"peerDependencies": {
"typescript": "*"
@ -2940,61 +2929,6 @@
}
}
},
"node_modules/vue-loading-overlay": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-6.0.6.tgz",
"integrity": "sha512-ZPrWawjCoNKGbCG9z4nePgbs/K9KXPa1j1oAJXP6T8FQho3NO+/chhjx4MLYFzfpwr+xkiQ8SNrV1kUG1bZPAw==",
"license": "MIT",
"engines": {
"node": ">=12.13.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-preloader": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/vue-preloader/-/vue-preloader-1.1.4.tgz",
"integrity": "sha512-XvBS4rzhPDJ/Ya+FOMVfkMK4maZuEn6/CED/Y94NTJiKnU/ASikixB2dYGgHfYhosRPdAVXIZJfetWnPbHgdJA==",
"license": "MIT",
"dependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": ">=14"
}
},
"node_modules/vue-toastification": {
"version": "2.0.0-rc.5",
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.2"
}
},
"node_modules/vue-typer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
"integrity": "sha512-o0n2F9yOnbdQak1OiPFbZonIzysL5jiS1OPgaEX0KnMlKqXRKi808QHRdoMuqw44oYQM/vtxCt3AaNb9OzKH1Q==",
"license": "MIT",
"dependencies": {
"lodash.split": "^4.4.2"
}
},
"node_modules/vue3-typer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vue3-typer/-/vue3-typer-1.0.0.tgz",
"integrity": "sha512-XliYAfNxPdu3D2zgiKzzr6I7TJR/Qs4tqmn5RbPxvn8Me3AjAabX90U1oizGlFrH/9qNEsyX0NMyDB0Z/NkqPQ==",
"license": "MIT",
"dependencies": {
"lodash.split": "^4.4.2",
"vue": "^3.2.37"
},
"peerDependencies": {
"vue": "^3.2"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -27,12 +27,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"vue": "^3.4.21",
"vue-loading-overlay": "^6.0.6",
"vue-preloader": "^1.1.4",
"vue-toastification": "^2.0.0-rc.5",
"vue-typer": "^1.2.0",
"vue3-typer": "^1.0.0"
"vue": "^3.4.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 496 KiB

View File

@ -1,21 +0,0 @@
{
"name": "Pablo TJ",
"short_name": "Pablo TJ",
"icons": [
{
"src": "/favicon.ico/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicon.ico/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "Pablo TJ",
"name": "Pablot TJ",
"icons": [
{
"src": "images/favicon-96x96.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "images/web-app-manifest-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "images/web-app-manifest-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#3b1070",
"background_color": "#3b1070"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,177 +1,156 @@
<script lang="ts" setup>
import {ref} from "vue";
import {useProfile} from '@/composables/useProfile.ts'
import {useNavigation} from '@/composables/useNavigation.ts'
import {VuePreloader} from 'vue-preloader';
import '../node_modules/vue-preloader/dist/style.css'
import avatarBot from '@/assets/avatar-bot.png'
// 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'
import EducationSection from "@/components/sections/EducationSection.vue";
import CertificationsSection from "@/components/sections/CertificationSection.vue";
import config from "@/config";
// Composables
const {profile, loading} = useProfile()
const {scrollToSection} = useNavigation()
// State
const isChatOpen = ref(false)
const navigationSections = [
{id: 'hero', label: 'Inicio', enabled: config.sections.heroEnabled},
{id: 'about', label: 'Sobre mí', enabled: config.sections.aboutEnabled},
{id: 'experience', label: 'Experiencia', enabled: config.sections.experienceEnabled},
{id: 'projects', label: 'Proyectos', enabled: config.sections.projectsEnabled},
{id: 'skills', label: 'Habilidades', enabled: config.sections.skillsEnabled},
{id: 'education', label: 'Educación', enabled: config.sections.educationEnabled},
{id: 'certification', label: 'Certificaciones', enabled: config.sections.certificationsEnabled},
{id: 'contact', label: 'Contacto', enabled: config.sections.contactEnabled}
]
function toggleChat() {
isChatOpen.value = !isChatOpen.value
}
</script>
<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">
<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"
/>
<VuePreloader
:loading-speed="25"
:transition-speed="1400"
background-color="#091a28"
color="#ffffff"
transition-type="fade-up"
@loading-is-over="loading"
@transition-is-over="loading"
>
<div class="preloader-content">
<img :src="avatarBot" alt="Logo" class="logo rounded-full"/>
<p>Preparando el Porfolio ... </p>
<!-- Chat Messages -->
<ChatMessages
:messages="messages"
:isLoading="isLoading"
ref="chatMessages"
/>
<!-- Input Form -->
<ChatInput
:input="input"
:isLoading="isLoading"
@update:input="input = $event"
@send-message="handleSubmit"
/>
<!-- Tech Stack Display -->
<TechStack :techStack="techStack" />
<!-- Footer -->
<AppFooter />
</div>
</VuePreloader>
<!-- Navigation -->
<AppNavigation
:sections="navigationSections"
:profile="profile?.profile"
@navigate="scrollToSection"
/>
<!-- Main Content -->
<main>
<!-- Hero Section -->
<HeroSection v-if="config.sections.heroEnabled"
id="hero"
:profile="profile?.profile"
/>
<!-- About Section -->
<AboutSection v-if="config.sections.aboutEnabled"
id="about"
:profile="profile?.profile"
/>
<!-- Experience Section -->
<ExperienceSection v-if="config.sections.experienceEnabled"
id="experience"
:experience="profile?.experience"
/>
<!-- Projects Section -->
<ProjectsSection v-if="config.sections.projectsEnabled"
id="projects"
:projects="profile?.projects"
/>
<!-- Skills Section -->
<SkillsSection v-if="config.sections.skillsEnabled"
id="skills"
:skillGroups="profile?.skills"
/>
<!-- Education Section -->
<EducationSection v-if="config.sections.educationEnabled"
id="education"
:education="profile?.education"
/>
<!-- Education Section -->
<CertificationsSection v-if="config.sections.certificationsEnabled"
id="certification"
:certification="profile?.certifications"
/>
<!-- Contact Section -->
<ContactSection v-if="config.sections.contactEnabled"
id="contact"
:profile="profile?.profile"
/>
</main>
<!-- Footer -->
<AppFooter :profile="profile?.profile"/>
<!-- Floating Chat Button -->
<ChatFloatingButton v-if="config.sections.chatEnabled" @toggle-chat="toggleChat"/>
<!-- Chat Popup -->
<ChatPopup
v-if="isChatOpen"
:chatConfig="profile?.chatbot"
@close="toggleChat"
/>
</div>
</div>
</template>
<style scoped>
.preloader-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #fff;
text-align: center;
}
<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'
.logo {
width: 100px;
height: 100px;
margin-bottom: 1rem;
animation: bounce 2s infinite;
}
const isDark = ref(true)
const isOnline = ref(navigator.onLine)
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
// Composables
const { findBestResponse } = useKnowledgeBase()
const {
messages,
input,
isLoading,
sendMessage: sendChatMessage,
handleSubmit
} = useChat(findBestResponse)
p {
font-size: 1.1rem;
opacity: 0.8;
margin-bottom: 2rem;
}
const chatMessages = ref(null)
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
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 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()
}
})
}
</style>
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)
})
</script>

View File

@ -0,0 +1,44 @@
<template>
<footer class="mt-12 pt-8 border-t border-white/10 text-center text-gray-400">
<div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<div class="flex items-center space-x-4">
<a
href="https://github.com/tu-usuario"
target="_blank"
class="hover:text-white transition-colors"
>
<Github class="w-5 h-5" />
</a>
<a
href="https://linkedin.com/in/tu-perfil"
target="_blank"
class="hover:text-white transition-colors"
>
<Linkedin class="w-5 h-5" />
</a>
<a
href="mailto:tu.email@ejemplo.com"
class="hover:text-white transition-colors"
>
<Mail class="w-5 h-5" />
</a>
</div>
<div class="text-sm">
<p>&copy; {{ currentYear }} Tu Nombre. Hecho con y Vue.js</p>
</div>
<div class="text-xs">
<p>Versión 1.0.0 Node.js {{ nodeVersion }}</p>
</div>
</div>
</footer>
</template>
<script setup>
import { computed } from 'vue'
import { Github, Linkedin, Mail } from 'lucide-vue-next'
const currentYear = computed(() => new Date().getFullYear())
const nodeVersion = '18+'
</script>

View File

@ -0,0 +1,57 @@
<template>
<header class="border-b border-white/10 backdrop-blur-sm bg-black/20 sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
<img src="/src/assets/avatar-bot.png" alt="Avatar" class=" rounded-full object-cover" />
</div>
<div>
<h1 class="text-xl font-bold">Pablo de la Torre Jamardo</h1>
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-1">
<div
class="w-2 h-2 rounded-full"
:class="isOnline ? 'bg-green-400' : 'bg-red-400'"
></div>
<span class="text-xs text-gray-300">
{{ isOnline ? 'Online' : 'Offline' }}
</span>
</div>
<span class="text-xs text-gray-400"></span>
<span class="text-xs text-gray-300">Virtual Me, Powered by Code</span>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<button
@click="$emit('toggle-theme')"
class="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
:title="isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro'"
>
<component :is="isDark ? 'Sun' : 'Moon'" class="w-5 h-5" />
</button>
<a
href="https://github.com/tu-usuario/ai-portfolio-chat"
target="_blank"
class="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
title="Ver código en GitHub"
>
<Github class="w-5 h-5" />
</a>
</div>
</div>
</header>
</template>
<script setup>
import { Bot, Sun, Moon, Github } from 'lucide-vue-next'
defineProps({
isDark: Boolean,
isOnline: Boolean
})
defineEmits(['toggle-theme'])
</script>

View File

@ -0,0 +1,65 @@
<template>
<form @submit.prevent="handleSubmit" class="relative">
<div class="flex space-x-2">
<div class="flex-1 relative">
<input
:value="input"
@input="$emit('update:input', $event.target.value)"
:disabled="isLoading"
placeholder="Pregúntame sobre mi experiencia, habilidades, proyectos..."
class="w-full px-4 py-3 pr-12 glass-effect rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder-gray-400 transition-all"
@keydown.enter.prevent="handleSubmit"
/>
<!-- Character counter -->
<div class="absolute right-3 top-1/2 transform -translate-y-1/2 text-xs text-gray-500">
{{ input.length }}/500
</div>
</div>
<button
type="submit"
:disabled="isLoading || !input.trim() || input.length > 500"
class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:from-purple-600 hover:to-pink-600 transition-all transform hover:scale-105 active:scale-95"
>
<Send v-if="!isLoading" class="w-5 h-5" />
<div v-else class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</button>
</div>
<!-- Quick suggestions -->
<div v-if="!input && quickSuggestions.length > 0" class="flex flex-wrap gap-2 mt-3">
<button
v-for="suggestion in quickSuggestions.slice(0, 3)"
:key="suggestion"
@click="$emit('update:input', suggestion)"
class="px-3 py-1 text-sm bg-white/5 hover:bg-white/10 rounded-full border border-white/10 transition-colors"
>
{{ suggestion }}
</button>
</div>
</form>
</template>
<script setup>
import { Send } from 'lucide-vue-next'
const props = defineProps({
input: String,
isLoading: Boolean
})
const emit = defineEmits(['update:input', 'send-message'])
const quickSuggestions = [
'¿Cuál es tu experiencia?',
'¿Qué tecnologías usas?',
'Háblame de tus proyectos'
]
function handleSubmit() {
if (props.input.trim() && !props.isLoading && props.input.length <= 500) {
emit('send-message')
}
}
</script>

View File

@ -0,0 +1,92 @@
<template>
<div class="space-y-4 mb-6 max-h-96 overflow-y-auto" ref="messagesContainer">
<TransitionGroup name="chat-message" tag="div">
<div
v-for="message in messages"
:key="message.id"
class="flex items-start space-x-3"
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
>
<div class="flex-shrink-0">
<div
class="w-8 h-8 rounded-full flex items-center justify-center"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500'
: 'bg-gradient-to-r from-purple-500 to-pink-500'"
>
<component :is="message.role === 'user' ? 'User' : 'Bot'" class="w-4 h-4" />
<img
:src="message.role === 'user' ? avatarUser : avatarBot"
alt="avatar"
class="w-8 h-8 rounded-full object-cover"
/>
</div>
</div>
<div
class="max-w-xs lg:max-w-md px-4 py-3 rounded-2xl"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white'
: 'glass-effect'"
>
<!-- Typing indicator -->
<div v-if="message.role === 'assistant' && message.typing" class="flex space-x-1">
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
</div>
<!-- Message content -->
<div v-else>
<div v-html="formatMessage(message.content)" class="prose prose-invert max-w-none"></div>
<div v-if="message.role === 'assistant'" class="text-xs text-gray-400 mt-2">
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
</div>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref, nextTick, watch } from 'vue'
import avatarUser from './../assets/avatar-user.jpg'
import avatarBot from './../assets/avatar-bot.png'
const props = defineProps({
messages: Array,
isLoading: Boolean
})
const messagesContainer = ref(null)
function formatMessage(content) {
return content.replace(/\n/g, '<br>')
}
function formatTime(timestamp) {
if (!timestamp) return ''
return new Date(timestamp).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
// Watch for new messages and scroll to bottom
watch(() => props.messages.length, () => {
nextTick(() => scrollToBottom())
})
// Expose scrollToBottom method
defineExpose({
scrollToBottom
})
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="mt-8 p-6 glass-effect rounded-xl">
<h3 class="text-lg font-semibold mb-4 flex items-center">
<Code class="w-5 h-5 mr-2 text-purple-400" />
Stack Tecnológico Principal
</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="(tech, index) in techStack"
:key="tech"
class="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm border border-purple-500/30 hover:bg-purple-500/30 transition-colors cursor-default"
:style="{ animationDelay: `${index * 50}ms` }"
>
{{ tech }}
</span>
</div>
</div>
</template>
<script setup>
import { Code } from 'lucide-vue-next'
defineProps({
techStack: Array
})
</script>

View File

@ -0,0 +1,64 @@
<template>
<div class="text-center mb-8 animate-fade-in">
<div class="mb-6">
<div class="w-24 h-24 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full mx-auto mb-4 flex items-center justify-center animate-pulse-slow">
<img src="/src/assets/avatar-bot.png" alt="Avatar" class="rounded-full object-cover" />
</div>
<h2 class="text-3xl font-bold mb-2 gradient-text">
¡Hola! Soy tu asistente personal de portfolio
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto">
Pregúntame sobre mi experiencia, habilidades, proyectos o cualquier detalle técnico.
¡Estoy listo para charlar y ayudarte a descubrir mi perfil profesional!
</p>
</div>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<button
v-for="(suggestion, index) in quickSuggestions"
:key="suggestion.text"
@click="$emit('send-message', suggestion.text)"
class="p-4 glass-effect rounded-xl transition-all hover:scale-105 hover:bg-white/15 text-left group"
:style="{ animationDelay: `${index * 100}ms` }"
>
<component
:is="suggestion.icon"
class="w-6 h-6 mb-2 text-purple-400 group-hover:text-purple-300 transition-colors"
/>
<h3 class="font-semibold mb-1 text-white group-hover:text-purple-100 transition-colors">
{{ suggestion.title }}
</h3>
<p class="text-sm text-gray-400 group-hover:text-gray-300 transition-colors">
{{ suggestion.text }}
</p>
</button>
</div>
<!-- Stats -->
<div class="flex justify-center space-x-8 text-sm text-gray-400">
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">5+</div>
<div>Años Experiencia</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">50+</div>
<div>Proyectos</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">15+</div>
<div>Tecnologías</div>
</div>
</div>
</div>
</template>
<script setup>
import { Bot, Briefcase, Code, Rocket, GraduationCap, Mail, DollarSign } from 'lucide-vue-next'
defineProps({
quickSuggestions: Array
})
defineEmits(['send-message'])
</script>

View File

@ -1,22 +0,0 @@
<script lang="ts" setup>
import {MessageCircle} from 'lucide-vue-next'
defineEmits(['toggle-chat'])
</script>
<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>

View File

@ -1,148 +0,0 @@
<script lang="ts" setup>
import {computed, nextTick, onMounted, ref} from 'vue'
import {Send, X} from 'lucide-vue-next'
import {useChatService} from '@/services/ChatService.ts'
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>
<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>

View File

@ -1,86 +0,0 @@
<script lang="ts" setup>
import {computed} from 'vue'
import {Github, Globe, Linkedin} from 'lucide-vue-next'
import config from "@/config";
import type {Profile} from '@/domain/models/Profile'
defineProps<{
profile: Profile
}>()
const currentYear = computed(() => new Date().getFullYear())
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
portfolio: Globe
}
return icons[platform] || Globe
}
</script>
<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">{{ profile?.name }}</h3>
<p class="text-gray-300 mb-4">
{{ profile?.title }}
</p>
<div class="flex space-x-4">
<a
v-for="social in profile?.social"
:key="social.platform"
:href="social.url"
target="_blank"
class="text-gray-400 hover:text-white transition-colors"
>
<component :is="getSocialIcon(social.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 v-if="config.sections.aboutEnabled">
<a class="text-gray-300 hover:text-white transition-colors" href="#about">Sobre </a>
</li>
<li v-if="config.sections.experienceEnabled">
<a class="text-gray-300 hover:text-white transition-colors" href="#experience">Experiencia</a>
</li>
<li v-if="config.sections.projectsEnabled">
<a class="text-gray-300 hover:text-white transition-colors" href="#projects">Proyectos</a>
</li>
<li v-if="config.sections.skillsEnabled">
<a class="text-gray-300 hover:text-white transition-colors" href="#skills">Habilidades</a>
</li>
<li v-if="config.sections.contactEnabled">
<a class="text-gray-300 hover:text-white transition-colors" href="#contact">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>{{ profile?.email }}</p>
<p style="display:none">{{ profile?.phone }}</p>
<p>{{ profile?.location }}</p>
</div>
</div>
</div>
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>&copy; {{ currentYear }} {{ profile?.name }}. Todos los derechos reservados.</p>
<p class="mt-2 text-sm">Hecho con y Vue.js</p>
</div>
</div>
</footer>
</template>

View File

@ -1,82 +0,0 @@
<script lang="ts" setup>
import {ref} from 'vue'
import {Menu, X} from 'lucide-vue-next'
import type {Profile} from '@/domain/models/Profile'
defineProps<{
sections: Array,
profile: Profile
}>()
const emit = defineEmits(['navigate'])
const isMobileMenuOpen = ref(false)
function toggleMobileMenu() {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
function handleMobileNavigate(sectionId) {
emit('navigate', sectionId)
isMobileMenuOpen.value = false
}
</script>
<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">{{ profile.name.charAt(0).toUpperCase() }}</span>
</div>
<div class="flex flex-col">
<span class="font-bold text-gray-900 dark:text-white">{{ profile.name }}</span>
<span class="text-gray-500 dark:text-gray-300 text-sm">{{ profile.title }}</span>
</div>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<template v-for="section in sections">
<button
v-if="section.enabled === true"
:key="section.id"
class="text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 transition-colors font-medium"
@click="$emit('navigate', section.id)"
>
{{ section.label }}
</button>
</template>
</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">
<template v-for="section in sections">
<button
v-if="section.enabled === true"
:key="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"
@click="handleMobileNavigate(section.id)"
>
{{ section.label }}
</button>
</template>
</div>
</div>
</div>
</nav>
</template>

View File

@ -1,70 +0,0 @@
<script lang="ts" setup>
import {Mail, MapPin, Phone} from 'lucide-vue-next'
import type {Profile} from '@/domain/models/Profile'
defineProps<{
profile: Profile
}>()
</script>
<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
</h2>
<div class="grid md:grid-cols-1 gap-12 items-center">
<!-- Bio -->
<div>
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
{{ profile?.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">{{ profile?.location }}</span>
</div>
<div class="flex items-center space-x-3">
<Mail class="w-5 h-5 text-purple-600" />
<a :href="`mailto:${profile?.email}`" class="text-purple-600 hover:underline">
{{ profile?.email }}
</a>
</div>
<div class="flex items-center space-x-3" style="display:none">
<Phone class="w-5 h-5 text-purple-600" />
<span class="text-gray-700 dark:text-gray-300">{{ profile?.phone }}</span>
</div>
</div>
</div>
<!-- Stats -->
<div v-if="1===3" 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">7+</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">10+</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>

View File

@ -1,74 +0,0 @@
<script lang="ts" setup>
import {type Certification} from '@/domain/models/Certification.ts'
import {Award, Calendar} from 'lucide-vue-next'
defineProps<{
certification: Certification[]
}>()
</script>
<template>
<section id="certification" 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">
Certificaciones
</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="(cert, index) in certification"
:key="cert.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">
<Award 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">
{{ cert.name }}
</h3>
<h4 class="text-lg text-purple-600 dark:text-purple-400 font-semibold">
{{ cert.issuer }}
</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>{{ cert.date }}</span>
</div>
</div>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-4">
{{ cert.description }}
</p>
<!-- Technologies -->
<div class="flex flex-wrap gap-2 mb-4">
<span
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm"
>
{{ cert.credentialId }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@ -1,186 +0,0 @@
<script lang="ts" setup>
import {ref} from 'vue'
import {Github, Globe, Linkedin, Mail, MapPin, Phone} from 'lucide-vue-next'
import type {Profile} from '@/domain/models/Profile'
import { useToast } from "vue-toastification";
const toast = useToast();
defineProps<{
profile: Profile
}>()
const form = ref({
name: '',
email: '',
message: ''
})
const isSubmitting = ref(false)
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
portfolio: Globe
}
return icons[platform] || Globe
}
async function handleSubmit() {
isSubmitting.value = true
try {
const payload = {
from: form.value.email,
subject: form.value.name,
body: form.value.message
}
const response = await fetch(`${import.meta.env.VITE_MAIL_API_URL}/mail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
throw new Error(`Error en el envío: ${response.status}`)
}
form.value = {
name: '',
email: '',
message: ''
}
toast.success("✅ ¡Mensaje enviado correctamente! Te responderé pronto.")
} catch (error) {
toast.error("❌ Hubo un error al enviar el mensaje. Inténtalo de nuevo.")
} finally {
isSubmitting.value = false
}
}
</script>
<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:${profile?.email}`" class="text-purple-600 hover:underline">
{{ profile?.email }}
</a>
</div>
</div>
<div v-if="profile?.phone" 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 style="display:none">
<div class="font-semibold text-gray-900 dark:text-white">Teléfono</div>
<span class="text-gray-600 dark:text-gray-300">{{ profile?.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">{{ profile?.location }}</span>
</div>
</div>
</div>
<!-- Social Links -->
<div class="flex space-x-4">
<a
v-for="social in profile?.social"
:key="social.platform"
:href="social.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(social.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>

View File

@ -1,74 +0,0 @@
<script lang="ts" setup>
import {Calendar, GraduationCap} from 'lucide-vue-next'
import type {Education} from '@/domain/models/Education'
defineProps<{
education: Education[]
}>()
</script>
<template>
<section id="education" 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">
Educación
</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="(edu, index) in education"
:key="edu.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">
<GraduationCap 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">
{{ edu.degree }}
</h3>
<h4 class="text-lg text-purple-600 dark:text-purple-400 font-semibold">
{{ edu.institution }}
</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>{{ edu.period }}</span>
</div>
</div>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-4">
{{ edu.description }}
</p>
<!-- Technologies -->
<div class="flex flex-wrap gap-2 mb-4">
<span
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm"
>
{{ edu.grade }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@ -1,94 +0,0 @@
<script lang="ts" setup>
import {Briefcase, Calendar, CheckCircle, MapPin} from 'lucide-vue-next'
import type {Experience} from '@/domain/models/Experience'
defineProps<{
experience: Experience[]
}>()
</script>
<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>

View File

@ -1,100 +0,0 @@
<script lang="ts" setup>
import {ChevronDown, Github, Globe, Linkedin} from 'lucide-vue-next'
import avatarUser from '@/assets/avatar-bot.png'
import config from "@/config";
import type {Profile} from '@/domain/models/Profile'
defineProps<{
profile: Profile
}>()
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
portfolio: Globe
}
return icons[platform] || Globe
}
function scrollToSection(sectionId) {
document.getElementById(sectionId)?.scrollIntoView({behavior: 'smooth'})
}
</script>
<template>
<section
class="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-200 to-pink-200 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
:alt="profile?.name"
:src="avatarUser"
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">
{{ profile?.name }}
</h1>
<h2 class="text-2xl md:text-3xl text-purple-600 dark:text-purple-400 font-semibold mb-6">
<VueTyper
:erase-delay='250'
:erase-on-complete='false'
:pre-erase-delay='2000'
:pre-type-delay='100'
:repeat='Infinity'
:shuffle='false'
:text="profile?.title ? [profile.title] : ['Cargando...']"
:type-delay='100'
caret-animation='smooth'
erase-style='clear'
initial-action='typing'
></VueTyper>
</h2>
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
{{ profile?.subtitle }}
</p>
<!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<button v-if="config.sections.projectsEnabled"
@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 v-if="config.sections.contactEnabled"
@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 gap-4 mb-12">
<a
v-for="social in profile?.social"
:key="social.platform"
:href="social.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(social.platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300"/>
</a>
</div>
<!-- Scroll Indicator -->
<div class="flex flex-col sm:flex-row justify-center animate-bounce">
<ChevronDown class="w-8 h-8 text-gray-400" />
</div>
</div>
</div>
</section>
</template>

View File

@ -1,102 +0,0 @@
<script lang="ts" setup>
import {ExternalLink, Github} from 'lucide-vue-next'
import type {Project} from '@/domain/models/Project'
defineProps<{
projects: Project[]
}>()
</script>
<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.demo"
:href="project.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.repository"
:href="project.repository"
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>

View File

@ -1,67 +0,0 @@
<script lang="ts" setup>
import {Cloud, Code, Database, Server} from 'lucide-vue-next'
import {SkillGroup} from "@/domain/models/Skill";
defineProps<{
skillGroups: SkillGroup[]
}>()
function getCategoryIcon(category) {
const icons = {
frontend: Code,
backend: Server,
database: Database,
devops: Cloud
}
return icons[category] || Code
}
</script>
<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 in skillGroups"
:key="skillGroup.name"
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="skillGroup.icon" class="w-6 h-6 mr-2 text-purple-600"/>
{{ skillGroup.name }}
</h3>
<div class="space-y-4">
<div
v-for="skill in skillGroup.skills"
: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>

View File

@ -0,0 +1,74 @@
import { ref } from "vue"
export function useChat(findBestResponse) {
const messages = ref([])
const input = ref("")
const isLoading = ref(false)
async function sendMessage(text = null) {
const messageText = text || input.value.trim()
if (!messageText || isLoading.value) return
// Agregar mensaje del usuario
const userMessage = {
id: Date.now(),
role: "user",
content: messageText,
timestamp: Date.now(),
}
messages.value.push(userMessage)
// Limpiar input
input.value = ""
isLoading.value = true
// Agregar mensaje de typing
const typingMessage = {
id: Date.now() + 1,
role: "assistant",
content: "",
typing: true,
}
messages.value.push(typingMessage)
// Simular delay de respuesta realista
const delay = 1000 + Math.random() * 2000 // 1-3 segundos
setTimeout(() => {
// Remover mensaje de typing
messages.value.pop()
// Obtener respuesta de la base de conocimiento
const responseData = findBestResponse(messageText)
// Agregar respuesta real
const assistantMessage = {
id: Date.now() + 2,
role: "assistant",
content: responseData.content,
category: responseData.category,
timestamp: responseData.timestamp,
}
messages.value.push(assistantMessage)
isLoading.value = false
}, delay)
}
function handleSubmit() {
sendMessage()
}
function clearChat() {
messages.value = []
}
return {
messages,
input,
isLoading,
sendMessage,
handleSubmit,
clearChat,
}
}

View File

@ -0,0 +1,337 @@
import { ref } from "vue"
export function useKnowledgeBase() {
const knowledgeBase = ref({
experiencia: {
keywords: ["experiencia", "trabajo", "laboral", "años", "empresa", "puesto", "carrera"],
response: `
<strong>💼 Experiencia Profesional</strong><br><br>
<strong>Senior Full Stack Developer</strong> (2021 - Presente)<br>
<em>TechCorp Solutions</em><br>
Liderazgo de equipo de 5 desarrolladores<br>
Arquitectura de microservicios con Node.js y Docker<br>
Implementación de CI/CD reduciendo deploys en 80%<br>
Migración de aplicaciones legacy a arquitecturas modernas<br><br>
<strong>Frontend Developer</strong> (2019 - 2021)<br>
<em>StartupXYZ</em><br>
Desarrollo de SPAs con Vue.js y React<br>
Optimización de performance (Core Web Vitals)<br>
Colaboración estrecha con equipos UX/UI<br>
Implementación de testing automatizado<br><br>
<strong>Junior Developer</strong> (2018 - 2019)<br>
<em>DevAgency</em><br>
Desarrollo de APIs REST con Express.js<br>
Integración con bases de datos SQL y NoSQL<br>
Metodologías ágiles (Scrum/Kanban)<br>
Participación en code reviews y pair programming
`,
},
habilidades: {
keywords: ["habilidades", "tecnologías", "stack", "lenguajes", "frameworks", "dominas", "herramientas"],
response: `
<strong>🚀 Stack Tecnológico</strong><br><br>
<strong>Frontend (Avanzado):</strong><br>
Vue.js 3 (Composition API, Pinia, Nuxt.js)<br>
React 18 (Hooks, Context, Redux Toolkit, Next.js)<br>
TypeScript - Tipado fuerte y desarrollo escalable<br>
Tailwind CSS, SCSS - Diseño responsive y modular<br>
Webpack, Vite - Bundling y optimización<br><br>
<strong>Backend (Avanzado):</strong><br>
Node.js (Express, Fastify, NestJS)<br>
Python (Django, FastAPI) - APIs y microservicios<br>
Bases de datos: PostgreSQL, MongoDB, Redis<br>
GraphQL, REST APIs - Diseño de APIs escalables<br><br>
<strong>DevOps & Cloud (Intermedio-Avanzado):</strong><br>
Docker, Kubernetes - Containerización<br>
AWS (EC2, S3, Lambda, RDS) - Cloud computing<br>
CI/CD (GitHub Actions, Jenkins) - Automatización<br>
Nginx, Apache - Configuración de servidores<br><br>
<strong>Herramientas & Metodologías:</strong><br>
Git (GitFlow, conventional commits)<br>
Jest, Cypress - Testing automatizado<br>
Scrum, Kanban - Metodologías ágiles<br>
Figma, Adobe XD - Colaboración con diseño
`,
},
proyectos: {
keywords: ["proyectos", "desarrollado", "creado", "portfolio", "destacados", "aplicaciones"],
response: `
<strong>🎯 Proyectos Destacados</strong><br><br>
<strong>🛒 E-commerce Platform</strong><br>
<em>Plataforma completa de comercio electrónico</em><br>
+50,000 usuarios activos mensuales<br>
Integración con múltiples pasarelas de pago<br>
Panel de administración con analytics en tiempo real<br>
<strong>Tech:</strong> Vue 3, Node.js, PostgreSQL, Redis, Stripe<br>
<strong>Logros:</strong> 99.9% uptime, 2s tiempo de carga<br><br>
<strong>📊 Real-time Analytics Dashboard</strong><br>
<em>Dashboard empresarial con visualizaciones interactivas</em><br>
Procesamiento de +1M eventos/día<br>
Visualizaciones en tiempo real con WebSockets<br>
Exportación de reportes automatizada<br>
<strong>Tech:</strong> React, D3.js, Socket.io, InfluxDB<br>
<strong>Logros:</strong> Reducción de 70% en tiempo de análisis<br><br>
<strong>🏗 Microservices Architecture</strong><br>
<em>Migración de monolito a microservicios</em><br>
Arquitectura distribuida con 12 microservicios<br>
Implementación de Event Sourcing y CQRS<br>
Monitoreo con Prometheus y Grafana<br>
<strong>Tech:</strong> Node.js, Docker, Kubernetes, AWS EKS<br>
<strong>Logros:</strong> 60% reducción latencia, 99.95% disponibilidad<br><br>
<strong>🤖 AI Portfolio Chat</strong> (Este proyecto)<br>
<em>Portfolio interactivo con simulación de IA</em><br>
Chat inteligente sin dependencias externas<br>
Respuestas contextuales pre-programadas<br>
Diseño responsive y accesible<br>
<strong>Tech:</strong> Vue 3, Tailwind CSS, Vite<br>
<strong>Innovación:</strong> Portfolio que demuestra habilidades técnicas
`,
},
educacion: {
keywords: ["educación", "estudios", "universidad", "carrera", "certificaciones", "formación"],
response: `
<strong>🎓 Formación Académica</strong><br><br>
<strong>Ingeniería en Sistemas de Información</strong><br>
<em>Universidad Tecnológica Nacional (2014-2018)</em><br>
Especialización en Desarrollo de Software<br>
Proyecto final: Sistema de gestión hospitalaria<br>
Promedio: 8.5/10<br><br>
<strong>Certificaciones Profesionales:</strong><br>
<strong>AWS Certified Developer Associate</strong> (2022)<br>
<strong>MongoDB Certified Developer</strong> (2021)<br>
<strong>Certified Scrum Master (CSM)</strong> (2020)<br>
<strong>Google Cloud Professional Developer</strong> (2023)<br><br>
<strong>Formación Continua:</strong><br>
<strong>Arquitectura de Software</strong> - Platzi (2023)<br>
<strong>Advanced React Patterns</strong> - Epic React (2022)<br>
<strong>Microservices with Node.js</strong> - Udemy (2021)<br>
<strong>Machine Learning Fundamentals</strong> - Coursera (2023)<br><br>
<strong>Participación en Comunidad:</strong><br>
Speaker en VueConf Argentina 2022<br>
Contribuciones a proyectos open source<br>
Mentor en programas de coding bootcamps<br>
Organizador de meetups locales de JavaScript
`,
},
contacto: {
keywords: ["contacto", "email", "linkedin", "github", "cv", "ubicación", "teléfono"],
response: `
<strong>📞 Información de Contacto</strong><br><br>
<strong>Datos Principales:</strong><br>
<strong>Email:</strong> tu.email@ejemplo.com<br>
<strong>LinkedIn:</strong> <a href="https://linkedin.com/in/tu-perfil" target="_blank" class="text-blue-400 hover:underline">linkedin.com/in/tu-perfil</a><br>
<strong>GitHub:</strong> <a href="https://github.com/tu-usuario" target="_blank" class="text-blue-400 hover:underline">github.com/tu-usuario</a><br>
<strong>Portfolio:</strong> <a href="https://tu-portfolio.com" target="_blank" class="text-blue-400 hover:underline">tu-portfolio.com</a><br><br>
<strong>Ubicación & Disponibilidad:</strong><br>
<strong>Ubicación:</strong> Buenos Aires, Argentina<br>
<strong>Zona horaria:</strong> GMT-3 (Argentina)<br>
<strong>Modalidad:</strong> Remoto/Híbrido/Presencial<br>
<strong>Disponibilidad:</strong> Inmediata<br><br>
<strong>Idiomas:</strong><br>
<strong>Español:</strong> Nativo<br>
<strong>Inglés:</strong> Avanzado (C1) - Certificado Cambridge<br>
<strong>Portugués:</strong> Intermedio (B2)<br><br>
<strong>Horarios de Contacto:</strong><br>
Lunes a Viernes: 9:00 - 18:00 (GMT-3)<br>
Respuesta garantizada en menos de 24hs<br><br>
<em>¡No dudes en contactarme para discutir oportunidades laborales!</em>
`,
},
salario: {
keywords: ["salario", "sueldo", "pretensiones", "económicas", "remuneración", "dinero", "pago"],
response: `
<strong>💰 Expectativas Salariales</strong><br><br>
<strong>Rango Salarial (USD/mes):</strong><br>
<strong>Remoto Internacional:</strong> $4,000 - $6,000<br>
<strong>Empresas Locales:</strong> $2,500 - $4,000<br>
<strong>Freelance/Consultoría:</strong> $50 - $80/hora<br><br>
<strong>Factores que Considero:</strong><br>
Complejidad técnica del proyecto<br>
Responsabilidades de liderazgo<br>
Oportunidades de crecimiento profesional<br>
Beneficios adicionales (salud, vacaciones, etc.)<br>
Cultura y ambiente de trabajo<br>
Modalidad de trabajo (remoto/híbrido/presencial)<br><br>
<strong>Beneficios Valorados:</strong><br>
🏥 Cobertura médica completa<br>
📚 Presupuesto para capacitación y conferencias<br>
💻 Equipamiento de trabajo de calidad<br>
🏖 Días de vacaciones flexibles<br>
🚀 Stock options o participación en ganancias<br>
🏠 Flexibilidad horaria y trabajo remoto<br><br>
<strong>Modalidades de Contratación:</strong><br>
Relación de dependencia (preferida)<br>
Contrato por proyecto<br>
Consultoría a largo plazo<br><br>
<em>Estoy abierto a negociar un paquete integral que sea beneficioso para ambas partes. Lo más importante para es encontrar un proyecto desafiante con un equipo talentoso.</em>
`,
},
})
const defaultResponses = [
"Esa es una excelente pregunta. Como desarrollador senior con más de 5 años de experiencia, siempre busco mantenerme actualizado con las últimas tecnologías y mejores prácticas del desarrollo web.",
"Interesante punto. En mi experiencia trabajando tanto en startups como en empresas enterprise, he encontrado que la clave está en encontrar el equilibrio entre innovación y estabilidad.",
"Desde mi perspectiva técnica, considero fundamental evaluar cada herramienta y tecnología en función del contexto específico del proyecto y las necesidades del negocio.",
"Basándome en mi experiencia liderando equipos y desarrollando arquitecturas escalables, puedo decir que la comunicación y la documentación son tan importantes como el código.",
"Como alguien que ha migrado sistemas legacy y implementado arquitecturas modernas, he aprendido que la planificación y el testing son cruciales para el éxito de cualquier proyecto.",
]
function findBestResponse(message) {
const lowerMessage = message.toLowerCase()
// Buscar en la base de conocimiento
for (const [category, data] of Object.entries(knowledgeBase.value)) {
if (data.keywords.some((keyword) => lowerMessage.includes(keyword))) {
return {
content: data.response,
category: category,
timestamp: Date.now(),
}
}
}
// Respuestas contextuales específicas
if (lowerMessage.includes("react") || lowerMessage.includes("vue")) {
return {
content: `
<strong> React vs Vue.js - Mi Perspectiva</strong><br><br>
Tengo experiencia sólida con ambos frameworks y los uso según el contexto:<br><br>
<strong>React:</strong><br>
Ecosistema maduro y comunidad muy activa<br>
Hooks y Context API para gestión de estado elegante<br>
Ideal para aplicaciones complejas y equipos grandes<br>
Excelente para desarrollo de componentes reutilizables<br><br>
<strong>Vue.js:</strong><br>
Curva de aprendizaje más suave y sintaxis intuitiva<br>
Composition API muy potente (similar a React Hooks)<br>
Excelente para desarrollo rápido y prototipado<br>
Mejor integración con proyectos existentes<br><br>
<strong>Mi Recomendación:</strong><br>
<em>React:</em> Para SPAs complejas, equipos grandes, ecosistema robusto<br>
<em>Vue:</em> Para desarrollo ágil, equipos pequeños, integración gradual<br><br>
<em>En mi experiencia, ambos son excelentes herramientas. La elección depende del proyecto, equipo y contexto específico.</em>
`,
category: "tecnologias",
timestamp: Date.now(),
}
}
if (lowerMessage.includes("node") || lowerMessage.includes("backend") || lowerMessage.includes("servidor")) {
return {
content: `
<strong>🔧 Desarrollo Backend con Node.js</strong><br><br>
Mi experiencia en backend se centra principalmente en el ecosistema JavaScript:<br><br>
<strong>Frameworks y Librerías:</strong><br>
<strong>Express.js:</strong> Framework minimalista, ideal para APIs REST<br>
<strong>Fastify:</strong> Alto rendimiento, excelente para microservicios<br>
<strong>NestJS:</strong> Arquitectura escalable, perfecto para aplicaciones enterprise<br><br>
<strong>Bases de Datos:</strong><br>
<strong>PostgreSQL:</strong> Para datos relacionales complejos<br>
<strong>MongoDB:</strong> Para datos no estructurados y prototipado rápido<br>
<strong>Redis:</strong> Para caché y sesiones de usuario<br><br>
<strong>Arquitecturas que Manejo:</strong><br>
APIs REST con documentación OpenAPI/Swagger<br>
GraphQL para consultas flexibles<br>
Microservicios con comunicación asíncrona<br>
Event-driven architecture con message queues<br><br>
<strong>Mejores Prácticas:</strong><br>
Testing automatizado (Jest, Supertest)<br>
Validación de datos con Joi/Yup<br>
Logging estructurado con Winston<br>
Monitoreo con Prometheus y Grafana<br><br>
<em>Siempre enfocado en código limpio, escalable y bien documentado.</em>
`,
category: "backend",
timestamp: Date.now(),
}
}
if (lowerMessage.includes("docker") || lowerMessage.includes("kubernetes") || lowerMessage.includes("devops")) {
return {
content: `
<strong>🐳 DevOps y Containerización</strong><br><br>
Mi experiencia en DevOps se enfoca en automatización y escalabilidad:<br><br>
<strong>Containerización:</strong><br>
<strong>Docker:</strong> Creación de imágenes optimizadas multi-stage<br>
<strong>Docker Compose:</strong> Orquestación local y testing<br>
<strong>Kubernetes:</strong> Despliegue y escalado en producción<br><br>
<strong>CI/CD Pipelines:</strong><br>
<strong>GitHub Actions:</strong> Automatización completa de workflows<br>
<strong>Jenkins:</strong> Pipelines complejos para empresas<br>
Testing automatizado, build y deploy<br><br>
<strong>Cloud Platforms:</strong><br>
<strong>AWS:</strong> EC2, ECS, Lambda, RDS, S3<br>
<strong>Google Cloud:</strong> GKE, Cloud Functions, Cloud SQL<br>
Infrastructure as Code con Terraform<br><br>
<strong>Monitoreo y Observabilidad:</strong><br>
Prometheus + Grafana para métricas<br>
ELK Stack para logs centralizados<br>
Health checks y alertas automatizadas<br><br>
<em>Mi objetivo es crear pipelines que permitan deploys seguros y frecuentes, reduciendo el time-to-market.</em>
`,
category: "devops",
timestamp: Date.now(),
}
}
// Respuesta por defecto
const randomResponse = defaultResponses[Math.floor(Math.random() * defaultResponses.length)]
return {
content: randomResponse,
category: "general",
timestamp: Date.now(),
}
}
return {
findBestResponse,
knowledgeBase,
}
}

View File

@ -1,18 +0,0 @@
export function useNavigation() {
function scrollToSection(sectionId: string) {
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,
}
}

View File

@ -1,25 +0,0 @@
import {onMounted, ref} from "vue";
import {ProfileService} from "@/infrastructure/api/ProfileService";
import {GetProfile} from "@/domain/usecases/GetProfile";
const profileService = new ProfileService();
const getProfileUseCase = new GetProfile(profileService);
export function useProfile() {
const profile = ref<any>(null);
const loading = ref(false);
const error = ref<string | null>(null);
onMounted(async () => {
loading.value = true;
try {
profile.value = await getProfileUseCase.execute();
} catch (err: any) {
error.value = err.message;
} finally {
loading.value = false;
}
});
return {profile, loading};
}

View File

@ -1,32 +0,0 @@
interface AppConfig {
apiUrl: boolean;
sections: {
heroEnabled: boolean;
aboutEnabled: boolean;
experienceEnabled: boolean;
projectsEnabled: boolean;
skillsEnabled: boolean;
educationEnabled: boolean;
certificationsEnabled: boolean,
contactEnabled: boolean;
chatEnabled: boolean;
};
}
const config: AppConfig = {
apiUrl: import.meta.env.VITE_API_URL || "http://localhost:3000",
sections: {
heroEnabled: true,
aboutEnabled: true,
experienceEnabled: true,
projectsEnabled: true,
skillsEnabled: true,
educationEnabled: true,
certificationsEnabled: true,
contactEnabled: true,
chatEnabled: false,
}
};
export default config;

View File

@ -1,7 +0,0 @@
export interface Certification {
id: string;
name: string;
issuer?: string;
date?: string;
credentialId?: string;
}

View File

@ -1,8 +0,0 @@
export interface Education {
id: string;
institution: string;
degree: string;
period?: string;
description?: string;
grade?: string;
}

View File

@ -1,10 +0,0 @@
export interface Experience {
id: string;
company: string;
position: string;
period: string;
location?: string;
description?: string;
technologies?: string[];
achievements?: string[];
}

View File

@ -1,21 +0,0 @@
export interface SocialLinks {
url?: string;
platform?: string;
}
export interface Profile {
id: number;
name: string;
title: string;
subtitle?: string;
email: string;
phone?: string;
location?: string;
avatar?: string;
bio?: string;
social?: SocialLinks[];
}
export default class ProfileDefault {
}

View File

@ -1,10 +0,0 @@
export interface Project {
id: string;
title: string;
description?: string;
image?: string;
technologies?: string[];
features?: string[];
demo?: string;
repository?: string;
}

View File

@ -1,11 +0,0 @@
export interface Skill {
name: string;
level: number;
years: number;
}
export interface SkillGroup {
name: string;
icon?: string;
skills: Skill[];
}

View File

@ -1,32 +0,0 @@
import {ProfileService} from "@/infrastructure/api/ProfileService";
import {Profile} from "@/domain/models/Profile";
import {Experience} from "@/domain/models/Experience";
import {Project} from "@/domain/models/Project";
import {SkillGroup} from "@/domain/models/Skill";
import {Education} from "@/domain/models/Education";
import {Certification} from "@/domain/models/Certification";
export interface ProfileData {
profile: Profile;
experience: Experience[];
projects: Project[];
skills: SkillGroup[];
education: Education[];
certifications: Certification[];
}
export class GetProfile {
constructor(private readonly profileService: ProfileService) {
}
async execute(): Promise<ProfileData> {
const profile = await this.profileService.getProfile();
const experience = await this.profileService.getExperience(profile.id);
const projects = await this.profileService.getProjects(profile.id);
const skills = await this.profileService.getSkills(profile.id);
const education = await this.profileService.getEducation(profile.id);
const certifications = await this.profileService.getCertifications(profile.id);
return {profile, experience, projects, skills, education, certifications};
}
}

View File

@ -1,59 +0,0 @@
import {Profile} from "@/domain/models/Profile";
import {Experience} from "@/domain/models/Experience";
import {Project} from "@/domain/models/Project";
import {SkillGroup} from "@/domain/models/Skill";
import {Education} from "@/domain/models/Education";
import {Certification} from "@/domain/models/Certification";
import profileMockData from "@/infrastructure/mock/profile.json";
import experienceMockData from "@/infrastructure/mock/experience.json";
import educationMockData from "@/infrastructure/mock/education.json";
import certificationMockData from "@/infrastructure/mock/certification.json";
import skillMockData from "@/infrastructure/mock/skill.json";
import projectMockData from "@/infrastructure/mock/project.json";
const useMock = import.meta.env.VITE_USE_MOCK === "true";
export class ProfileService {
async getProfile(): Promise<Profile> {
if (useMock) return profileMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${import.meta.env.VITE_PORTFOLIO_SLUG}`);
if (!res.ok) console.log(new Error("Error fetching personal data"));
return await res.json();
}
async getExperience(profileId: number): Promise<Experience[]> {
if (useMock) return experienceMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${profileId}/experience`);
if (!res.ok) console.log(new Error("Error fetching experience data"));
return await res.json();
}
async getProjects(profileId: number): Promise<Project[]> {
if (useMock) return projectMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${profileId}/projects`);
if (!res.ok) console.log(new Error("Error fetching projects data"));
return await res.json();
}
async getSkills(profileId: number): Promise<SkillGroup[]> {
if (useMock) return skillMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${profileId}/skills`);
if (!res.ok) console.log(new Error("Error fetching skills data"));
return await res.json();
}
async getEducation(profileId: number): Promise<Education[]> {
if (useMock) return educationMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${profileId}/education`);
if (!res.ok) console.log(new Error("Error fetching education data"));
return await res.json();
}
async getCertifications(profileId: number): Promise<Certification[]> {
if (useMock) return certificationMockData.content;
const res = await fetch(`${import.meta.env.VITE_API_URL}/profiles/${profileId}/certifications`);
if (!res.ok) console.log(new Error("Error fetching certifications data"));
return await res.json();
}
}

View File

@ -1,25 +0,0 @@
{
"content": [
{
"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"
}
]
}

View File

@ -1,83 +0,0 @@
{
"content": {
"welcome": {
"message": "¡Hola! 👋 Soy el asistente virtual de Pablo. ¿Te interesa saber sobre su trabajo en backend, despliegue automatizado o LLMs locales?",
"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 trabaja actualmente como Arquitecto de Software en la Xunta de Galicia, liderando desarrollos backend complejos. Anteriormente ha trabajado como consultor y freelance, siempre enfocado en eficiencia y automatización."
},
"skills": {
"keywords": [
"habilidades",
"tecnologías",
"stack",
"lenguajes",
"frameworks"
],
"response": "Domina tecnologías como Java (Spring Boot), Vue.js (Quasar), despliegue automatizado con Docker y GitHub Actions, y recientemente integración con modelos LLM offline."
},
"projects": {
"keywords": [
"proyectos",
"desarrollado",
"creado",
"portfolio",
"aplicaciones"
],
"response": "Ha desarrollado una app LLM offline en Java, un sistema de reservas autónomo para un hostel, y una plataforma completa de tramitación electrónica con firma digital y backend distribuido."
},
"contact": {
"keywords": [
"contacto",
"email",
"teléfono",
"linkedin",
"ubicación"
],
"response": "Puedes contactar a Pablo en pablo@pablotj.com o vía LinkedIn: linkedin.com/in/pablotj. Vive en Galicia, España."
},
"education": {
"keywords": [
"educación",
"estudios",
"universidad",
"carrera",
"certificaciones"
],
"response": "Estudió Ingeniería Informática en la Universidade de Vigo. Está certificado en arquitectura Java, GitOps y modelos LLM locales."
}
},
"fallback": [
"Buena pregunta. Pablo valora mucho el rendimiento y la mantenibilidad en sus proyectos.",
"Interesante. Él suele aplicar principios de arquitectura limpia y buenas prácticas DevOps.",
"Siempre busca mantener la simplicidad sin sacrificar funcionalidad o seguridad."
]
}
}

View File

@ -1,12 +0,0 @@
{
"content": [
{
"id": "edu1",
"institution": "CIFP Daniel Castelao, Vigo",
"degree": "Ciclo Formativo de Grado Superior en Desarrollo de Aplicaciones Multiplataforma",
"period": "2015 - 2017",
"description": "Formación técnica especializada en desarrollo de aplicaciones móviles y de escritorio multiplataforma, bases de datos, y programación orientada a objetos.",
"grade": "No aplicable"
}
]
}

View File

@ -1,77 +0,0 @@
{
"content": [
{
"id": "exp1",
"company": "Bahia Software",
"position": "Analista Programador Senior y Líder Tecnológico",
"period": "2019 - Presente",
"location": "O Milladoiro, Galicia, España / Remoto",
"description": "Evolución desde programador junior hasta líder tecnológico. Responsable de análisis, desarrollo y liderazgo técnico.",
"technologies": [
"Java 8",
"Java 11",
"Spring Boot",
"Spring Framework",
"Spring Security",
"Spring Cloud",
"Docker",
"Jenkins",
"Keycloak",
"Vue.js",
"Oracle DB",
"MariaDB",
"SOAP",
"REST",
"WebLogic",
"Tomcat",
"SonarQube",
"JMS",
"Arquitectura Hexagonal"
],
"achievements": [
"Liderazgo técnico en proyectos clave para la AMTEGA",
"Automatización de despliegues y calidad de código con Sonar y Jenkins"
]
},
{
"id": "exp2",
"company": "Optare Solutions",
"position": "Programador Junior",
"period": "2018 - 2019",
"location": "Vigo, Galicia, España",
"description": "Inicio profesional con beca FEUGA y posterior incorporación como programador junior. Participación en proyectos Java, migración de versiones y tareas puntuales relacionadas con VoIP y AWS.",
"technologies": [
"Java",
"Java 8",
"Java 11",
"Angular",
"VoIP",
"AWS"
],
"achievements": [
"Migración de aplicaciones de Java 8 a Java 11",
"Colaboración en desarrollo frontend con Angular",
"Participación en entornos productivos de telecomunicaciones"
]
},
{
"id": "exp3",
"company": "Kidcode",
"position": "Tutor de programación didáctica",
"period": "2017 - 2018",
"location": "Galicia, España",
"description": "Tutor de tiempo libre para iniciación a la programación en niños de entre 5 y 12 años. Uso de herramientas educativas como Arduino, Raspberry Pi, Lego y entornos low-code.",
"technologies": [
"Arduino",
"Raspberry Pi",
"Lego",
"Low-code"
],
"achievements": [
"Diseño de actividades didácticas adaptadas por edades",
"Fomento del pensamiento computacional en edades tempranas",
"Introducción práctica a la robótica educativa"
]
}
]
}

View File

@ -1,23 +0,0 @@
{
"content": {
"id": -1,
"name": "Pablo de la Torre",
"title": "Softaware Developer",
"subtitle": "Especializado en Java",
"email": "contact@pablotj.com",
"phone": "",
"location": "Pontevedra, Galicia, España",
"avatar": "assets/avatar-bot.png",
"bio": "Desarrollador de software con más de 7 años de experiencia. Apasionado por crear sistemas sólidos, automatizar tareas y trabajar con contenedores. Recientemente, he empezado a explorar modelos LLM locales como parte de mis intereses técnicos.",
"social": [
{
"url": "https://github.com/pablotj",
"platform": "github"
},
{
"url": "https://linkedin.com/in/pablotj",
"platform": "linkedin"
}
]
}
}

View File

@ -1,78 +0,0 @@
{
"content": [
{
"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"
},
"demo": "https://demo-ecommerce.com",
"repository": "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+"
},
"demo": "https://analytics-demo.com",
"repository": "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%"
},
"repository": "https://github.com/pablo/microservices"
}
]
}

View File

@ -1,201 +0,0 @@
{
"content": [
{
"name": "Backend",
"icon": "Server",
"skills": [
{
"name": "Java",
"level": 95,
"years": 7
},
{
"name": "Spring Boot",
"level": 90,
"years": 6
},
{
"name": "Spring Framework",
"level": 85,
"years": 6
},
{
"name": "Spring Security",
"level": 80,
"years": 5
},
{
"name": "Spring Cloud",
"level": 75,
"years": 3
},
{
"name": "Node.js",
"level": 40,
"years": 1
},
{
"name": "Python",
"level": 30,
"years": 1
},
{
"name": "JUnit",
"level": 80,
"years": 5
},
{
"name": "Mockito",
"level": 75,
"years": 4
}
]
},
{
"name": "DevOps",
"icon": "Server",
"skills": [
{
"name": "Docker",
"level": 90,
"years": 5
},
{
"name": "Jenkins",
"level": 85,
"years": 4
},
{
"name": "Keycloak",
"level": 80,
"years": 3
},
{
"name": "CI/CD",
"level": 85,
"years": 4
},
{
"name": "SonarQube",
"level": 75,
"years": 3
},
{
"name": "Linux",
"level": 85,
"years": 6
},
{
"name": "Git",
"level": 90,
"years": 6
},
{
"name": "SVN",
"level": 60,
"years": 4
},
{
"name": "Kubernetes",
"level": 40,
"years": 1
}
]
},
{
"name": "Base de Datos",
"icon": "Server",
"skills": [
{
"name": "Oracle",
"level": 85,
"years": 5
},
{
"name": "MariaDB",
"level": 80,
"years": 4
},
{
"name": "JMS",
"level": 75,
"years": 3
}
]
},
{
"name": "Frontend",
"icon": "Server",
"skills": [
{
"name": "Angular",
"level": 85,
"years": 5
},
{
"name": "Bootstrap",
"level": 75,
"years": 4
},
{
"name": "TypeScript",
"level": 70,
"years": 3
},
{
"name": "Vue.js",
"level": 40,
"years": 1
}
]
},
{
"name": "Methodologies",
"icon": "Server",
"skills": [
{
"name": "Agile",
"level": 85,
"years": 5
},
{
"name": "Scrum",
"level": 80,
"years": 4
},
{
"name": "Kanban",
"level": 70,
"years": 3
}
]
},
{
"name": "Monitoring",
"icon": "Server",
"skills": [
{
"name": "Prometheus",
"level": 30,
"years": 1
},
{
"name": "Grafana",
"level": 30,
"years": 1
}
]
},
{
"name": "Languages",
"icon": "Server",
"skills": [
{
"name": "Inglés",
"level": 80,
"years": 7
}
]
}
]
}

View File

@ -1,34 +0,0 @@
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 || []
}
}

12
src/main.js Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from "vue"
import App from "./App.vue"
import "./style.css"
const app = createApp(App)
// Global error handler
app.config.errorHandler = (err, vm, info) => {
console.error("Global error:", err, info)
}
app.mount("#app")

View File

@ -1,19 +0,0 @@
import {createApp} from "vue"
import "./style.css"
import VueTyper from 'vue3-typer'
import "vue3-typer/dist/vue-typer.css"
import App from "./App.vue";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
const app = createApp(App)
app.component('VueTyper', VueTyper)
app.use(Toast, {});
// Global error handler
app.config.errorHandler = (err, vm, info) => {
console.error("Global error:", err, info)
}
app.mount("#app")

View File

@ -1,77 +0,0 @@
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,
}
}

18
src/shims-vue.d.ts vendored
View File

@ -1,18 +0,0 @@
declare module '*.vue' {
import type {DefineComponent} from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '*.json' {
const value: any
export default value
}
declare module '*.css' {
const content: Record<string, string>
export default content
}
declare module '*.png'
declare module '*.jpg'
declare module '*.svg'

View File

@ -2,12 +2,6 @@
@tailwind components;
@tailwind utilities;
@layer utilities {
.border-border {
border-color: hsl(var("#4c1d95"));
}
}
@layer base {
html {
scroll-behavior: smooth;
@ -58,14 +52,12 @@
}
::-webkit-scrollbar-thumb {
/*background: rgba(147, 51, 234, 0.5);*/
background: rgb(9, 26, 40); /* Blue-500 */
background: rgba(147, 51, 234, 0.5);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
/*background: rgba(147, 51, 234, 0.7);*7
background: rgba(59, 130, 246, 0.7); /* Blue-500 */
background: rgba(147, 51, 234, 0.7);
}
/* Animaciones personalizadas */

View File

@ -7,84 +7,77 @@ module.exports = {
theme: {
extend: {
colors: {
background: "#0f0f1f", // fondo oscuro elegante
foreground: "#f8f8f2",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "#8b5cf6", // violeta principal
50: "#f3ebff",
100: "#e0d7ff",
200: "#c0aaff",
300: "#a078ff",
400: "#8b5cf6",
500: "#7c3aed",
600: "#6d28d9",
700: "#5b21b6",
800: "#4c1d95",
900: "#3b1070",
foreground: "#ffffff",
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
},
secondary: {
DEFAULT: "#c084fc", // púrpura suave para CTA
foreground: "#ffffff",
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "#d8b4fe", // acento claro violeta
foreground: "#1f1f2e",
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
muted: {
DEFAULT: "#a78bfa", // violeta pálido
foreground: "#f8f8f2",
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "#1e1b2f", // card dark purple
foreground: "#f8f8f2",
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "#2c2345",
foreground: "#f8f8f2",
purple: {
400: "#a855f7",
500: "#9333ea",
600: "#7c3aed",
},
pink: {
500: "#ec4899",
600: "#db2777",
},
success: "#7dd3fc",
warning: "#facc15",
error: "#f87171",
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Fira Code", ...fontFamily.mono],
},
animation: {
"bounce-slow": "bounce 2s infinite",
"pulse-slow": "pulse 3s infinite",
"fade-in": "fadeIn 0.6s ease-in-out",
"float": "float 4s ease-in-out infinite",
"wiggle": "wiggle 0.8s ease-in-out infinite",
"fade-in": "fadeIn 0.5s ease-in-out",
},
keyframes: {
fadeIn: {
"0%": { opacity: "0", transform: "translateY(10px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
float: {
"0%, 100%": {transform: "translateY(0)"},
"50%": {transform: "translateY(-10px)"},
},
wiggle: {
"0%, 100%": {transform: "rotate(-3deg)"},
"50%": {transform: "rotate(3deg)"},
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
},
borderRadius: {
lg: "1rem",
md: "0.75rem",
sm: "0.5rem",
},
boxShadow: {
"card-light": "0 8px 16px rgba(139,92,246,0.2)",
"card-dark": "0 8px 20px rgba(0,0,0,0.5)",
glow: "0 0 20px rgba(139,92,246,0.6)",
},
transitionProperty: {
height: "height",
spacing: "margin, padding",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},

View File

@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"ESNext",
"DOM"
],
"types": [
"vite/client"
],
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue"
]
}

View File

@ -1,10 +1,10 @@
import {defineConfig} from "vite"
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import {resolve} from "path"
import { resolve } from "path"
export default defineConfig({
plugins: [vue()],
base: process.env.NODE_ENV === "production" ? "/" : "/",
base: process.env.NODE_ENV === "production" ? "/ai-portfolio-chat/" : "/",
resolve: {
alias: {
"@": resolve(__dirname, "src"),
@ -26,13 +26,5 @@ export default defineConfig({
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
//rewrite: path => path.replace(/^\/api/, '')
}
}
},
})