refactor: restructure project to classic MVC pattern
This commit is contained in:
parent
a28728af2a
commit
2a8d5d093c
10
README.md
10
README.md
@ -77,9 +77,9 @@ npm run deploy
|
||||
|
||||
## ⚙️ Personalización
|
||||
|
||||
### 1. Información Personal
|
||||
### 1. Información Profile
|
||||
|
||||
Edita `src/composables/useKnowledgeBase.js`:
|
||||
Edita `src/composables/useKnowledgeBase.ts`:
|
||||
|
||||
\`\`\`javascript
|
||||
const knowledgeBase = ref({
|
||||
@ -148,10 +148,10 @@ ai-portfolio-chat/
|
||||
│ │ ├── TechStack.vue
|
||||
│ │ └── AppFooter.vue
|
||||
│ ├── composables/ # Lógica reutilizable
|
||||
│ │ ├── useKnowledgeBase.js
|
||||
│ │ └── useChat.js
|
||||
│ │ ├── useKnowledgeBase.ts
|
||||
│ │ └── useChat.ts
|
||||
│ ├── App.vue # Componente principal
|
||||
│ ├── main.js # Punto de entrada
|
||||
│ ├── main.ts # Punto de entrada
|
||||
│ └── style.css # Estilos globales
|
||||
├── public/ # Archivos estáticos
|
||||
├── .github/workflows/ # GitHub Actions
|
||||
|
475
app.vue
475
app.vue
@ -1,475 +0,0 @@
|
||||
<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 mí:</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);*/
|
||||
background: rgba(59, 130, 246, 0.5); /* Blue-500 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
/*background: rgba(147, 51, 234, 0.7);*7
|
||||
background: rgba(59, 130, 246, 0.7); /* Blue-500 */
|
||||
}
|
||||
</style>
|
29
index.html
29
index.html
@ -2,30 +2,25 @@
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link href="/avatar-bot.png" rel="icon" type="image/png"/>
|
||||
<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 content="Portfolio de Pablo de la Torre" name="description"/>
|
||||
<meta name="keywords" content="desarrollador, full stack, vue.js, react, node.js, portfolio, programador" />
|
||||
<meta name="author" content="Tu Nombre" />
|
||||
|
||||
<meta content="Pablo de la Torre" name="author"/>
|
||||
<link href="/manifest.json" rel="manifest"/>
|
||||
<meta content="#3b1070" name="theme-color"/>
|
||||
<meta content="#3b1070" name="background-color"/>
|
||||
<meta content="#3b1070" name="apple-mobile-web-app-status-bar-style">
|
||||
<!-- 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 content="https://pablotj.com" property="og:url"/>
|
||||
<meta content="Portfolio | Pablot TJ" property="og:title"/>
|
||||
<meta content="Portfolio de Pablo de la Torre" property="og:description"/>
|
||||
|
||||
<!-- 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>
|
||||
<title>Portfolio | Pablot TJ</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="/src/main.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
11
nginx.conf
Normal file
11
nginx.conf
Normal file
@ -0,0 +1,11 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
58
package-lock.json
generated
58
package-lock.json
generated
@ -9,7 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21"
|
||||
"vue": "^3.4.21",
|
||||
"vue-loading-overlay": "^6.0.6",
|
||||
"vue-preloader": "^1.1.4",
|
||||
"vue-typer": "^1.2.0",
|
||||
"vue3-typer": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
@ -1855,6 +1859,12 @@
|
||||
"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",
|
||||
@ -2929,6 +2939,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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-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",
|
||||
|
@ -27,7 +27,11 @@
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21"
|
||||
"vue": "^3.4.21",
|
||||
"vue-loading-overlay": "^6.0.6",
|
||||
"vue-preloader": "^1.1.4",
|
||||
"vue-typer": "^1.2.0",
|
||||
"vue3-typer": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
|
BIN
public/avatar-bot.png
Normal file
BIN
public/avatar-bot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 372 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "Pablo TJ",
|
||||
"name": "Pablot TJ",
|
||||
"icons": [
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3b1070",
|
||||
"background_color": "#3b1070"
|
||||
}
|
228
src/App.vue
228
src/App.vue
@ -1,69 +1,10 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
|
||||
<!-- Navigation -->
|
||||
<AppNavigation
|
||||
:sections="navigationSections"
|
||||
@navigate="scrollToSection"
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<HeroSection
|
||||
id="hero"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
|
||||
<!-- About Section -->
|
||||
<AboutSection
|
||||
id="about"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
|
||||
<!-- Experience Section -->
|
||||
<ExperienceSection
|
||||
id="experience"
|
||||
:experience="portfolioData.experience"
|
||||
/>
|
||||
|
||||
<!-- Projects Section -->
|
||||
<ProjectsSection
|
||||
id="projects"
|
||||
:projects="portfolioData.projects"
|
||||
/>
|
||||
|
||||
<!-- Skills Section -->
|
||||
<SkillsSection
|
||||
id="skills"
|
||||
:skills="portfolioData.skills"
|
||||
/>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<ContactSection
|
||||
id="contact"
|
||||
:personal="portfolioData.personal"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<AppFooter :personal="portfolioData.personal" />
|
||||
|
||||
<!-- Floating Chat Button -->
|
||||
<ChatFloatingButton @toggle-chat="toggleChat" />
|
||||
|
||||
<!-- Chat Popup -->
|
||||
<ChatPopup
|
||||
v-if="isChatOpen"
|
||||
:chatConfig="portfolioData.chatbot"
|
||||
@close="toggleChat"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { usePortfolioData } from '@/composables/usePortfolioData'
|
||||
import { useNavigation } from '@/composables/useNavigation'
|
||||
<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'
|
||||
@ -76,28 +17,161 @@ 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 { portfolioData, loadPortfolioData } = usePortfolioData()
|
||||
const { scrollToSection } = useNavigation()
|
||||
const {profile, loading} = useProfile()
|
||||
const {scrollToSection} = useNavigation()
|
||||
|
||||
// State
|
||||
const isChatOpen = ref(false)
|
||||
|
||||
const navigationSections = [
|
||||
{ id: 'hero', label: 'Inicio' },
|
||||
{ id: 'about', label: 'Sobre mí' },
|
||||
{ id: 'experience', label: 'Experiencia' },
|
||||
{ id: 'projects', label: 'Proyectos' },
|
||||
{ id: 'skills', label: 'Habilidades' },
|
||||
{ id: 'contact', label: 'Contacto' }
|
||||
{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
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPortfolioData()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preloader-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 1rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,44 +0,0 @@
|
||||
<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>© {{ 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>
|
@ -1,57 +0,0 @@
|
||||
<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>
|
@ -1,65 +0,0 @@
|
||||
<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>
|
@ -1,92 +0,0 @@
|
||||
<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>
|
@ -1,26 +0,0 @@
|
||||
<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>
|
@ -1,64 +0,0 @@
|
||||
<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>
|
@ -1,3 +1,9 @@
|
||||
<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
|
||||
@ -14,8 +20,3 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MessageCircle } from 'lucide-vue-next'
|
||||
|
||||
defineEmits(['toggle-chat'])
|
||||
</script>
|
||||
|
@ -1,3 +1,55 @@
|
||||
<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 -->
|
||||
@ -93,56 +145,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||||
import { X, Send } from 'lucide-vue-next'
|
||||
import { useChatService } from '@/services/ChatService'
|
||||
import avatarUser from '@/assets/avatar-user.jpg'
|
||||
import avatarBot from '@/assets/avatar-bot.png'
|
||||
|
||||
const props = defineProps({
|
||||
chatConfig: Object
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const { messages, input, isLoading, sendMessage: sendChatMessage } = useChatService(props.chatConfig)
|
||||
const messagesContainer = ref(null)
|
||||
|
||||
const showQuickActions = computed(() => {
|
||||
return messages.value.length <= 1 && props.chatConfig?.welcome?.quickActions
|
||||
})
|
||||
|
||||
function sendMessage(text) {
|
||||
sendChatMessage(text)
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (input.value.trim()) {
|
||||
sendMessage(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Send welcome message
|
||||
if (props.chatConfig?.welcome?.message) {
|
||||
setTimeout(() => {
|
||||
messages.value.push({
|
||||
id: Date.now(),
|
||||
role: 'assistant',
|
||||
content: props.chatConfig.welcome.message
|
||||
})
|
||||
nextTick(() => scrollToBottom())
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</template>
|
@ -1,22 +1,44 @@
|
||||
<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">{{ personal.name }}</h3>
|
||||
<h3 class="text-xl font-bold mb-4">{{ profile?.name }}</h3>
|
||||
<p class="text-gray-300 mb-4">
|
||||
{{ personal.title }} especializado en crear experiencias web excepcionales.
|
||||
{{ profile?.title }}
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
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(platform)" class="w-5 h-5" />
|
||||
<component :is="getSocialIcon(social.platform)" class="w-5 h-5"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,11 +47,21 @@
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Enlaces Rápidos</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#about" class="text-gray-300 hover:text-white transition-colors">Sobre mí</a></li>
|
||||
<li><a href="#experience" class="text-gray-300 hover:text-white transition-colors">Experiencia</a></li>
|
||||
<li><a href="#projects" class="text-gray-300 hover:text-white transition-colors">Proyectos</a></li>
|
||||
<li><a href="#skills" class="text-gray-300 hover:text-white transition-colors">Habilidades</a></li>
|
||||
<li><a href="#contact" class="text-gray-300 hover:text-white transition-colors">Contacto</a></li>
|
||||
<li v-if="config.sections.aboutEnabled">
|
||||
<a class="text-gray-300 hover:text-white transition-colors" href="#about">Sobre mí</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>
|
||||
|
||||
@ -37,38 +69,18 @@
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Contacto</h3>
|
||||
<div class="space-y-2 text-gray-300">
|
||||
<p>{{ personal.email }}</p>
|
||||
<p>{{ personal.phone }}</p>
|
||||
<p>{{ personal.location }}</p>
|
||||
<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>© {{ currentYear }} {{ personal.name }}. Todos los derechos reservados.</p>
|
||||
<p>© {{ currentYear }} {{ profile?.name }}. Todos los derechos reservados.</p>
|
||||
<p class="mt-2 text-sm">Hecho con ❤️ y Vue.js</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
</script>
|
||||
|
@ -1,3 +1,27 @@
|
||||
<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">
|
||||
@ -5,21 +29,26 @@
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-bold text-sm">P</span>
|
||||
<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>
|
||||
<span class="font-bold text-gray-900 dark:text-white">Pablo de la Torre Jamardo</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="$emit('navigate', section.id)"
|
||||
class="text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 transition-colors font-medium"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
<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 -->
|
||||
@ -35,38 +64,19 @@
|
||||
<!-- Mobile Navigation -->
|
||||
<div v-if="isMobileMenuOpen" class="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="handleMobileNavigate(section.id)"
|
||||
class="text-left px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
<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>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Menu, X } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
sections: Array
|
||||
})
|
||||
|
||||
const emit = defineEmits(['navigate'])
|
||||
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
function handleMobileNavigate(sectionId) {
|
||||
emit('navigate', sectionId)
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
@ -1,3 +1,15 @@
|
||||
<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">
|
||||
@ -5,40 +17,40 @@
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Sobre mí
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
||||
|
||||
<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">
|
||||
{{ personal.bio }}
|
||||
{{ 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">{{ personal.location }}</span>
|
||||
<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:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
<a :href="`mailto:${profile?.email}`" class="text-purple-600 hover:underline">
|
||||
{{ profile?.email }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<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">{{ personal.phone }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ profile?.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<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">5+</div>
|
||||
<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">50+</div>
|
||||
<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">
|
||||
@ -56,10 +68,3 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MapPin, Mail, Phone } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
</script>
|
||||
|
74
src/components/sections/CertificationSection.vue
Normal file
74
src/components/sections/CertificationSection.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<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>
|
||||
|
@ -1,3 +1,52 @@
|
||||
<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'
|
||||
|
||||
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
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log('Form submitted:', form.value)
|
||||
|
||||
// Reset form
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
isSubmitting.value = false
|
||||
|
||||
// Show success message (you could use a toast notification)
|
||||
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="contact" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
@ -25,19 +74,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Email</div>
|
||||
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
<a :href="`mailto:${profile?.email}`" class="text-purple-600 hover:underline">
|
||||
{{ profile?.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<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>
|
||||
<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">{{ personal.phone }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ profile?.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -47,7 +96,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Ubicación</div>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ personal.location }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ profile?.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,25 +104,28 @@
|
||||
<!-- Social Links -->
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
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(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
<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">
|
||||
<p class="m-5">¡Hola! Por el momento mi servidor SMTP está de vacaciones 😅.</p>
|
||||
<p class="m-5">Si quieres contactarme, envíame un correo electrónico directamente y prometo responderte
|
||||
rápido.</p>
|
||||
<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
|
||||
<input disabled
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
@ -85,7 +137,7 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
<input disabled
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
@ -97,7 +149,7 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea
|
||||
<textarea disabled
|
||||
v-model="form.message"
|
||||
rows="4"
|
||||
required
|
||||
@ -107,7 +159,7 @@
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
:disabled="isSubmitting || 1===1"
|
||||
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' }}
|
||||
@ -120,51 +172,3 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Mail, Phone, MapPin, Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log('Form submitted:', form.value)
|
||||
|
||||
// Reset form
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
isSubmitting.value = false
|
||||
|
||||
// Show success message (you could use a toast notification)
|
||||
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
|
||||
}
|
||||
</script>
|
||||
|
74
src/components/sections/EducationSection.vue
Normal file
74
src/components/sections/EducationSection.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<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>
|
||||
|
@ -1,3 +1,12 @@
|
||||
<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">
|
||||
@ -83,10 +92,3 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Briefcase, Calendar, MapPin, CheckCircle } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
experience: Array
|
||||
})
|
||||
</script>
|
||||
|
@ -1,38 +1,74 @@
|
||||
<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-50 to-pink-50 dark:from-gray-900 dark:to-purple-900 pt-16">
|
||||
<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
|
||||
:src="personal.avatar"
|
||||
:alt="personal.name"
|
||||
: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">
|
||||
{{ personal.name }}
|
||||
{{ profile?.name }}
|
||||
</h1>
|
||||
|
||||
<h2 class="text-2xl md:text-3xl text-purple-600 dark:text-purple-400 font-semibold mb-6">
|
||||
{{ personal.title }}
|
||||
<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">
|
||||
{{ personal.subtitle }}
|
||||
{{ profile?.subtitle }}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
||||
<button
|
||||
<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
|
||||
<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"
|
||||
>
|
||||
@ -41,20 +77,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="flex justify-center space-x-6">
|
||||
<div class="flex justify-center gap-4 mb-12">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
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(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
<component :is="getSocialIcon(social.platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<div class="flex flex-col sm:flex-row justify-center animate-bounce">
|
||||
<ChevronDown class="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
@ -62,24 +98,3 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Github, Linkedin, Twitter, Globe, ChevronDown } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
function scrollToSection(sectionId) {
|
||||
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
@ -1,3 +1,12 @@
|
||||
<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">
|
||||
@ -64,8 +73,8 @@
|
||||
<!-- Links -->
|
||||
<div class="flex space-x-2">
|
||||
<a
|
||||
v-if="project.links.demo"
|
||||
:href="project.links.demo"
|
||||
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"
|
||||
>
|
||||
@ -73,8 +82,8 @@
|
||||
Demo
|
||||
</a>
|
||||
<a
|
||||
v-if="project.links.github"
|
||||
:href="project.links.github"
|
||||
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"
|
||||
>
|
||||
@ -85,15 +94,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ExternalLink, Github } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
projects: Array
|
||||
})
|
||||
</script>
|
||||
|
@ -1,3 +1,22 @@
|
||||
<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">
|
||||
@ -8,18 +27,18 @@
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div
|
||||
v-for="(skillGroup, category) in skills"
|
||||
:key="category"
|
||||
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="getCategoryIcon(category)" class="w-6 h-6 mr-2 text-purple-600" />
|
||||
{{ getCategoryName(category) }}
|
||||
<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"
|
||||
v-for="skill in skillGroup.skills"
|
||||
:key="skill.name"
|
||||
class="space-y-2"
|
||||
>
|
||||
@ -46,30 +65,3 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Code, Server, Database, Cloud } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
skills: Object
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
const icons = {
|
||||
frontend: Code,
|
||||
backend: Server,
|
||||
database: Database,
|
||||
devops: Cloud
|
||||
}
|
||||
return icons[category] || Code
|
||||
}
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
frontend: 'Frontend',
|
||||
backend: 'Backend',
|
||||
database: 'Base de Datos',
|
||||
devops: 'DevOps'
|
||||
}
|
||||
return names[category] || category
|
||||
}
|
||||
</script>
|
||||
|
@ -1,74 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,337 +0,0 @@
|
||||
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 mí 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,
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export function useNavigation() {
|
||||
function scrollToSection(sectionId) {
|
||||
function scrollToSection(sectionId: string) {
|
||||
const element = document.getElementById(sectionId)
|
||||
if (element) {
|
||||
const offset = 80 // Account for fixed header
|
@ -1,30 +0,0 @@
|
||||
import { ref } from "vue"
|
||||
import { PortfolioRepository } from "@/infrastructure/repositories/PortfolioRepository"
|
||||
|
||||
export function usePortfolioData() {
|
||||
const portfolioData = ref({})
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const portfolioRepository = new PortfolioRepository()
|
||||
|
||||
async function loadPortfolioData() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
portfolioData.value = await portfolioRepository.getPortfolioData()
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error("Error loading portfolio data:", err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
portfolioData,
|
||||
isLoading,
|
||||
error,
|
||||
loadPortfolioData,
|
||||
}
|
||||
}
|
25
src/composables/useProfile.ts
Normal file
25
src/composables/useProfile.ts
Normal file
@ -0,0 +1,25 @@
|
||||
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};
|
||||
}
|
32
src/config.ts
Normal file
32
src/config.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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;
|
@ -1,228 +0,0 @@
|
||||
{
|
||||
"personal": {
|
||||
"name": "Pablo de la Torre Jamardo",
|
||||
"title": "Senior Full Stack Developer",
|
||||
"subtitle": "Especializado en Vue.js, React y Node.js",
|
||||
"email": "pablo.delatorre@ejemplo.com",
|
||||
"phone": "+54 11 1234-5678",
|
||||
"location": "Buenos Aires, Argentina",
|
||||
"avatar": "/src/assets/avatar-bot.png",
|
||||
"bio": "Desarrollador Full Stack con más de 5 años de experiencia creando aplicaciones web escalables y modernas. Especializado en JavaScript, Vue.js, React y arquitecturas de microservicios.",
|
||||
"social": {
|
||||
"github": "https://github.com/pablo-delatorre",
|
||||
"linkedin": "https://linkedin.com/in/pablo-delatorre",
|
||||
"twitter": "https://twitter.com/pablo_dev",
|
||||
"portfolio": "https://pablo-portfolio.com"
|
||||
}
|
||||
},
|
||||
"experience": [
|
||||
{
|
||||
"id": "exp1",
|
||||
"company": "TechCorp Solutions",
|
||||
"position": "Senior Full Stack Developer",
|
||||
"period": "2021 - Presente",
|
||||
"location": "Buenos Aires, Argentina",
|
||||
"description": "Liderazgo de equipo de 5 desarrolladores, arquitectura de microservicios con Node.js y Docker, implementación de CI/CD reduciendo deploys en 80%.",
|
||||
"technologies": ["Vue.js", "Node.js", "Docker", "AWS", "PostgreSQL"],
|
||||
"achievements": [
|
||||
"Migración de aplicaciones legacy a arquitecturas modernas",
|
||||
"Reducción del 80% en tiempo de deployment",
|
||||
"Implementación de testing automatizado"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "exp2",
|
||||
"company": "StartupXYZ",
|
||||
"position": "Frontend Developer",
|
||||
"period": "2019 - 2021",
|
||||
"location": "Buenos Aires, Argentina",
|
||||
"description": "Desarrollo de SPAs con Vue.js y React, optimización de performance y colaboración con equipos UX/UI.",
|
||||
"technologies": ["Vue.js", "React", "TypeScript", "Tailwind CSS"],
|
||||
"achievements": [
|
||||
"Optimización de Core Web Vitals",
|
||||
"Implementación de design system",
|
||||
"Mejora del 40% en performance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "exp3",
|
||||
"company": "DevAgency",
|
||||
"position": "Junior Developer",
|
||||
"period": "2018 - 2019",
|
||||
"location": "Buenos Aires, Argentina",
|
||||
"description": "Desarrollo de APIs REST con Express.js, integración con bases de datos y metodologías ágiles.",
|
||||
"technologies": ["Node.js", "Express.js", "MongoDB", "Git"],
|
||||
"achievements": [
|
||||
"Desarrollo de 15+ APIs REST",
|
||||
"Participación en metodologías ágiles",
|
||||
"Contribución a proyectos open source"
|
||||
]
|
||||
}
|
||||
],
|
||||
"projects": [
|
||||
{
|
||||
"id": "proj1",
|
||||
"title": "E-commerce Platform",
|
||||
"description": "Plataforma completa de comercio electrónico con más de 50,000 usuarios activos mensuales",
|
||||
"image": "/placeholder.svg?height=300&width=400",
|
||||
"technologies": ["Vue.js", "Node.js", "PostgreSQL", "Redis", "Stripe"],
|
||||
"features": [
|
||||
"Integración con múltiples pasarelas de pago",
|
||||
"Panel de administración con analytics",
|
||||
"Sistema de inventario en tiempo real"
|
||||
],
|
||||
"metrics": {
|
||||
"users": "50,000+",
|
||||
"uptime": "99.9%",
|
||||
"loadTime": "2s"
|
||||
},
|
||||
"links": {
|
||||
"demo": "https://demo-ecommerce.com",
|
||||
"github": "https://github.com/pablo/ecommerce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "proj2",
|
||||
"title": "Real-time Analytics Dashboard",
|
||||
"description": "Dashboard empresarial con visualizaciones interactivas y procesamiento de más de 1M eventos/día",
|
||||
"image": "/placeholder.svg?height=300&width=400",
|
||||
"technologies": ["React", "D3.js", "Socket.io", "InfluxDB", "Node.js"],
|
||||
"features": ["Visualizaciones en tiempo real", "Exportación de reportes automatizada", "Alertas personalizables"],
|
||||
"metrics": {
|
||||
"events": "1M+/día",
|
||||
"reduction": "70% tiempo análisis",
|
||||
"users": "500+"
|
||||
},
|
||||
"links": {
|
||||
"demo": "https://analytics-demo.com",
|
||||
"github": "https://github.com/pablo/analytics"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "proj3",
|
||||
"title": "Microservices Architecture",
|
||||
"description": "Migración de monolito a arquitectura de microservicios con 12 servicios distribuidos",
|
||||
"image": "/placeholder.svg?height=300&width=400",
|
||||
"technologies": ["Node.js", "Docker", "Kubernetes", "AWS EKS", "MongoDB"],
|
||||
"features": ["Event Sourcing y CQRS", "Monitoreo con Prometheus", "Auto-scaling automático"],
|
||||
"metrics": {
|
||||
"services": "12",
|
||||
"latencyReduction": "60%",
|
||||
"availability": "99.95%"
|
||||
},
|
||||
"links": {
|
||||
"github": "https://github.com/pablo/microservices"
|
||||
}
|
||||
}
|
||||
],
|
||||
"skills": {
|
||||
"frontend": [
|
||||
{ "name": "Vue.js", "level": 95, "years": 4 },
|
||||
{ "name": "React", "level": 90, "years": 3 },
|
||||
{ "name": "TypeScript", "level": 85, "years": 3 },
|
||||
{ "name": "Tailwind CSS", "level": 90, "years": 2 },
|
||||
{ "name": "Nuxt.js", "level": 80, "years": 2 }
|
||||
],
|
||||
"backend": [
|
||||
{ "name": "Node.js", "level": 95, "years": 5 },
|
||||
{ "name": "Express.js", "level": 90, "years": 4 },
|
||||
{ "name": "NestJS", "level": 80, "years": 2 },
|
||||
{ "name": "Python", "level": 75, "years": 2 },
|
||||
{ "name": "GraphQL", "level": 70, "years": 1 }
|
||||
],
|
||||
"database": [
|
||||
{ "name": "PostgreSQL", "level": 85, "years": 4 },
|
||||
{ "name": "MongoDB", "level": 80, "years": 3 },
|
||||
{ "name": "Redis", "level": 75, "years": 2 }
|
||||
],
|
||||
"devops": [
|
||||
{ "name": "Docker", "level": 85, "years": 3 },
|
||||
{ "name": "AWS", "level": 80, "years": 2 },
|
||||
{ "name": "Kubernetes", "level": 70, "years": 1 },
|
||||
{ "name": "CI/CD", "level": 85, "years": 3 }
|
||||
]
|
||||
},
|
||||
"education": [
|
||||
{
|
||||
"id": "edu1",
|
||||
"institution": "Universidad Tecnológica Nacional",
|
||||
"degree": "Ingeniería en Sistemas de Información",
|
||||
"period": "2014 - 2018",
|
||||
"description": "Especialización en Desarrollo de Software. Proyecto final: Sistema de gestión hospitalaria.",
|
||||
"grade": "8.5/10"
|
||||
}
|
||||
],
|
||||
"certifications": [
|
||||
{
|
||||
"id": "cert1",
|
||||
"name": "AWS Certified Developer Associate",
|
||||
"issuer": "Amazon Web Services",
|
||||
"date": "2022",
|
||||
"credentialId": "AWS-123456"
|
||||
},
|
||||
{
|
||||
"id": "cert2",
|
||||
"name": "MongoDB Certified Developer",
|
||||
"issuer": "MongoDB Inc.",
|
||||
"date": "2021",
|
||||
"credentialId": "MONGO-789012"
|
||||
},
|
||||
{
|
||||
"id": "cert3",
|
||||
"name": "Certified Scrum Master",
|
||||
"issuer": "Scrum Alliance",
|
||||
"date": "2020",
|
||||
"credentialId": "CSM-345678"
|
||||
}
|
||||
],
|
||||
"chatbot": {
|
||||
"welcome": {
|
||||
"message": "¡Hola! 👋 Soy el asistente virtual de Pablo. Puedo contarte sobre su experiencia, habilidades, proyectos y más. ¿Qué te gustaría saber?",
|
||||
"quickActions": [
|
||||
{
|
||||
"text": "¿Cuál es su experiencia laboral?",
|
||||
"category": "experience"
|
||||
},
|
||||
{
|
||||
"text": "¿Qué tecnologías domina?",
|
||||
"category": "skills"
|
||||
},
|
||||
{
|
||||
"text": "Cuéntame sobre sus proyectos",
|
||||
"category": "projects"
|
||||
},
|
||||
{
|
||||
"text": "¿Cómo puedo contactarlo?",
|
||||
"category": "contact"
|
||||
}
|
||||
]
|
||||
},
|
||||
"responses": {
|
||||
"experience": {
|
||||
"keywords": ["experiencia", "trabajo", "laboral", "empresa", "puesto", "carrera"],
|
||||
"response": "Pablo tiene más de 5 años de experiencia como desarrollador Full Stack. Actualmente es Senior Developer en TechCorp Solutions, donde lidera un equipo de 5 desarrolladores y ha implementado arquitecturas de microservicios que redujeron los tiempos de deployment en un 80%. Anteriormente trabajó en StartupXYZ optimizando performance y en DevAgency desarrollando APIs REST."
|
||||
},
|
||||
"skills": {
|
||||
"keywords": ["habilidades", "tecnologías", "stack", "lenguajes", "frameworks"],
|
||||
"response": "Pablo domina un amplio stack tecnológico: Frontend con Vue.js (95%), React (90%) y TypeScript (85%). En Backend maneja Node.js (95%), Express.js (90%) y Python (75%). También tiene experiencia en bases de datos como PostgreSQL y MongoDB, y en DevOps con Docker, AWS y Kubernetes."
|
||||
},
|
||||
"projects": {
|
||||
"keywords": ["proyectos", "desarrollado", "creado", "portfolio", "aplicaciones"],
|
||||
"response": "Entre sus proyectos destacados están: una plataforma de e-commerce con +50,000 usuarios activos, un dashboard de analytics en tiempo real que procesa +1M eventos/día, y la migración de un monolito a arquitectura de microservicios que mejoró la disponibilidad al 99.95%."
|
||||
},
|
||||
"contact": {
|
||||
"keywords": ["contacto", "email", "teléfono", "linkedin", "ubicación"],
|
||||
"response": "Puedes contactar a Pablo por email: pablo.delatorre@ejemplo.com, teléfono: +54 11 1234-5678. También está en LinkedIn: linkedin.com/in/pablo-delatorre y GitHub: github.com/pablo-delatorre. Se encuentra en Buenos Aires, Argentina."
|
||||
},
|
||||
"education": {
|
||||
"keywords": ["educación", "estudios", "universidad", "carrera", "certificaciones"],
|
||||
"response": "Pablo es Ingeniero en Sistemas de Información por la UTN (2014-2018) con especialización en Desarrollo de Software. Tiene certificaciones de AWS Developer Associate, MongoDB Certified Developer y Certified Scrum Master."
|
||||
}
|
||||
},
|
||||
"fallback": [
|
||||
"Esa es una excelente pregunta. Pablo siempre busca mantenerse actualizado con las últimas tecnologías.",
|
||||
"Interesante punto. En su experiencia, ha encontrado que la clave está en el equilibrio entre innovación y estabilidad.",
|
||||
"Desde su perspectiva técnica, considera fundamental evaluar cada herramienta según el contexto del proyecto."
|
||||
]
|
||||
}
|
||||
}
|
7
src/domain/models/Certification.ts
Normal file
7
src/domain/models/Certification.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface Certification {
|
||||
id: string;
|
||||
name: string;
|
||||
issuer?: string;
|
||||
date?: string;
|
||||
credentialId?: string;
|
||||
}
|
8
src/domain/models/Education.ts
Normal file
8
src/domain/models/Education.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Education {
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
period?: string;
|
||||
description?: string;
|
||||
grade?: string;
|
||||
}
|
10
src/domain/models/Experience.ts
Normal file
10
src/domain/models/Experience.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface Experience {
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
period: string;
|
||||
location?: string;
|
||||
description?: string;
|
||||
technologies?: string[];
|
||||
achievements?: string[];
|
||||
}
|
21
src/domain/models/Profile.ts
Normal file
21
src/domain/models/Profile.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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 {
|
||||
|
||||
}
|
10
src/domain/models/Project.ts
Normal file
10
src/domain/models/Project.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
technologies?: string[];
|
||||
features?: string[];
|
||||
demo?: string;
|
||||
repository?: string;
|
||||
}
|
11
src/domain/models/Skill.ts
Normal file
11
src/domain/models/Skill.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface Skill {
|
||||
name: string;
|
||||
level: number;
|
||||
years: number;
|
||||
}
|
||||
|
||||
export interface SkillGroup {
|
||||
name: string;
|
||||
icon?: string;
|
||||
skills: Skill[];
|
||||
}
|
32
src/domain/usecases/GetProfile.ts
Normal file
32
src/domain/usecases/GetProfile.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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};
|
||||
}
|
||||
}
|
59
src/infrastructure/api/ProfileService.ts
Normal file
59
src/infrastructure/api/ProfileService.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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();
|
||||
}
|
||||
}
|
25
src/infrastructure/mock/certification.json
Normal file
25
src/infrastructure/mock/certification.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
83
src/infrastructure/mock/chatbot.json
Normal file
83
src/infrastructure/mock/chatbot.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"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."
|
||||
]
|
||||
}
|
||||
}
|
12
src/infrastructure/mock/education.json
Normal file
12
src/infrastructure/mock/education.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
77
src/infrastructure/mock/experience.json
Normal file
77
src/infrastructure/mock/experience.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
23
src/infrastructure/mock/profile.json
Normal file
23
src/infrastructure/mock/profile.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
78
src/infrastructure/mock/project.json
Normal file
78
src/infrastructure/mock/project.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
201
src/infrastructure/mock/skill.json
Normal file
201
src/infrastructure/mock/skill.json
Normal file
@ -0,0 +1,201 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import portfolioConfig from "@/data/portfolio-config.json"
|
||||
|
||||
export class PortfolioRepository {
|
||||
async getPortfolioData() {
|
||||
// Simulate API call delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
return portfolioConfig
|
||||
}
|
||||
|
||||
async getPersonalInfo() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.personal
|
||||
}
|
||||
|
||||
async getExperience() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.experience
|
||||
}
|
||||
|
||||
async getProjects() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.projects
|
||||
}
|
||||
|
||||
async getSkills() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.skills
|
||||
}
|
||||
|
||||
async getEducation() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.education
|
||||
}
|
||||
|
||||
async getCertifications() {
|
||||
const data = await this.getPortfolioData()
|
||||
return data.certifications
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import { createApp } from "vue"
|
||||
import App from "./App.vue"
|
||||
import {createApp} from "vue"
|
||||
import "./style.css"
|
||||
import VueTyper from 'vue3-typer'
|
||||
import "vue3-typer/dist/vue-typer.css"
|
||||
import App from "./App.vue";
|
||||
|
||||
const app = createApp(App)
|
||||
app.component('VueTyper', VueTyper)
|
||||
|
||||
// Global error handler
|
||||
app.config.errorHandler = (err, vm, info) => {
|
@ -1,5 +1,5 @@
|
||||
import { ref } from "vue"
|
||||
import { ChatRepository } from "@/infrastructure/repositories/ChatRepository"
|
||||
import {ref} from "vue"
|
||||
import {ChatRepository} from "@/infrastructure/repositories/ChatRepository"
|
||||
|
||||
export function useChatService(chatConfig) {
|
||||
const messages = ref([])
|
18
src/shims-vue.d.ts
vendored
Normal file
18
src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
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'
|
@ -2,6 +2,12 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.border-border {
|
||||
border-color: hsl(var("#4c1d95"));
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
@ -53,7 +59,7 @@
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
/*background: rgba(147, 51, 234, 0.5);*/
|
||||
background: rgba(59, 130, 246, 0.5); /* Blue-500 */
|
||||
background: rgb(9, 26, 40); /* Blue-500 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
@ -7,79 +7,86 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
background: "#0f0f1f", // fondo oscuro elegante
|
||||
foreground: "#f8f8f2",
|
||||
primary: {
|
||||
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",
|
||||
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",
|
||||
},
|
||||
secondary: {
|
||||
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))",
|
||||
DEFAULT: "#c084fc", // púrpura suave para CTA
|
||||
foreground: "#ffffff",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
DEFAULT: "#d8b4fe", // acento claro violeta
|
||||
foreground: "#1f1f2e",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
muted: {
|
||||
DEFAULT: "#a78bfa", // violeta pálido
|
||||
foreground: "#f8f8f2",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
DEFAULT: "#1e1b2f", // card dark purple
|
||||
foreground: "#f8f8f2",
|
||||
},
|
||||
purple: {
|
||||
400: "#475569", // slate-600
|
||||
500: "#334155", // slate-700
|
||||
600: "#1e293b", // slate-800
|
||||
},
|
||||
pink: {
|
||||
500: "#0ea5e9", // sky-500 (azul vibrante, pero no chillón)
|
||||
600: "#0284c7", // sky-600
|
||||
popover: {
|
||||
DEFAULT: "#2c2345",
|
||||
foreground: "#f8f8f2",
|
||||
},
|
||||
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.5s ease-in-out",
|
||||
"fade-in": "fadeIn 0.6s ease-in-out",
|
||||
"float": "float 4s ease-in-out infinite",
|
||||
"wiggle": "wiggle 0.8s ease-in-out infinite",
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
"0%": { opacity: "0", transform: "translateY(10px)" },
|
||||
"100%": { opacity: "1", transform: "translateY(0)" },
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Inter", ...fontFamily.sans],
|
||||
float: {
|
||||
"0%, 100%": {transform: "translateY(0)"},
|
||||
"50%": {transform: "translateY(-10px)"},
|
||||
},
|
||||
wiggle: {
|
||||
"0%, 100%": {transform: "rotate(-3deg)"},
|
||||
"50%": {transform: "rotate(3deg)"},
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
};
|
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
@ -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" ? "/ai-portfolio-chat/" : "/",
|
||||
base: process.env.NODE_ENV === "production" ? "/" : "/",
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
@ -26,5 +26,13 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
//rewrite: path => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user