Initial functional version of the portfolio chatbot site

This commit is contained in:
2025-07-22 08:06:10 +02:00
parent 9eca12ebca
commit a28728af2a
21 changed files with 1484 additions and 205 deletions

View File

@@ -0,0 +1,21 @@
<template>
<div class="fixed bottom-6 right-6 z-50">
<button
@click="$emit('toggle-chat')"
class="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110 flex items-center justify-center"
>
<MessageCircle class="w-8 h-8" />
</button>
<!-- Notification Badge -->
<div class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs rounded-full flex items-center justify-center animate-pulse">
1
</div>
</div>
</template>
<script setup>
import { MessageCircle } from 'lucide-vue-next'
defineEmits(['toggle-chat'])
</script>

View File

@@ -0,0 +1,148 @@
<template>
<div class="fixed inset-0 z-50 flex items-end justify-end p-4">
<!-- Backdrop -->
<div
class="absolute inset-0 bg-black/20 backdrop-blur-sm"
@click="$emit('close')"
></div>
<!-- Chat Window -->
<div class="relative w-full max-w-md h-96 bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center space-x-3">
<img
:src="avatarBot"
alt="Assistant"
class="w-8 h-8 rounded-full"
/>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">Asistente Virtual</h3>
<p class="text-xs text-green-500">En línea</p>
</div>
</div>
<button
@click="$emit('close')"
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
>
<X class="w-5 h-5" />
</button>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
class="flex items-start space-x-2"
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
>
<img
:src="message.role === 'user' ? avatarUser : avatarBot"
alt="avatar"
class="w-6 h-6 rounded-full flex-shrink-0"
/>
<div
class="max-w-xs px-3 py-2 rounded-lg text-sm"
:class="message.role === 'user'
? 'bg-purple-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'"
>
<div v-if="message.typing" class="flex space-x-1">
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<div v-else v-html="message.content"></div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div v-if="showQuickActions" class="px-4 pb-2">
<div class="flex flex-wrap gap-2">
<button
v-for="action in chatConfig.welcome.quickActions"
:key="action.text"
@click="sendMessage(action.text)"
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-xs hover:bg-purple-200 dark:hover:bg-purple-800 transition-colors"
>
{{ action.text }}
</button>
</div>
</div>
<!-- Input -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex space-x-2">
<input
v-model="input"
@keydown.enter="handleSubmit"
:disabled="isLoading"
placeholder="Escribe tu pregunta..."
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
/>
<button
@click="handleSubmit"
:disabled="isLoading || !input.trim()"
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg transition-colors"
>
<Send class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, computed } from 'vue'
import { X, Send } from 'lucide-vue-next'
import { useChatService } from '@/services/ChatService'
import avatarUser from '@/assets/avatar-user.jpg'
import avatarBot from '@/assets/avatar-bot.png'
const props = defineProps({
chatConfig: Object
})
defineEmits(['close'])
const { messages, input, isLoading, sendMessage: sendChatMessage } = useChatService(props.chatConfig)
const messagesContainer = ref(null)
const showQuickActions = computed(() => {
return messages.value.length <= 1 && props.chatConfig?.welcome?.quickActions
})
function sendMessage(text) {
sendChatMessage(text)
nextTick(() => scrollToBottom())
}
function handleSubmit() {
if (input.value.trim()) {
sendMessage(input.value)
}
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
onMounted(() => {
// Send welcome message
if (props.chatConfig?.welcome?.message) {
setTimeout(() => {
messages.value.push({
id: Date.now(),
role: 'assistant',
content: props.chatConfig.welcome.message
})
nextTick(() => scrollToBottom())
}, 500)
}
})
</script>

View File

@@ -0,0 +1,74 @@
<template>
<footer class="bg-gray-900 text-white py-12">
<div class="container mx-auto px-4">
<div class="grid md:grid-cols-3 gap-8">
<!-- About -->
<div>
<h3 class="text-xl font-bold mb-4">{{ personal.name }}</h3>
<p class="text-gray-300 mb-4">
{{ personal.title }} especializado en crear experiencias web excepcionales.
</p>
<div class="flex space-x-4">
<a
v-for="(url, platform) in personal.social"
:key="platform"
:href="url"
target="_blank"
class="text-gray-400 hover:text-white transition-colors"
>
<component :is="getSocialIcon(platform)" class="w-5 h-5" />
</a>
</div>
</div>
<!-- Quick Links -->
<div>
<h3 class="text-xl font-bold mb-4">Enlaces Rápidos</h3>
<ul class="space-y-2">
<li><a href="#about" class="text-gray-300 hover:text-white transition-colors">Sobre </a></li>
<li><a href="#experience" class="text-gray-300 hover:text-white transition-colors">Experiencia</a></li>
<li><a href="#projects" class="text-gray-300 hover:text-white transition-colors">Proyectos</a></li>
<li><a href="#skills" class="text-gray-300 hover:text-white transition-colors">Habilidades</a></li>
<li><a href="#contact" class="text-gray-300 hover:text-white transition-colors">Contacto</a></li>
</ul>
</div>
<!-- Contact Info -->
<div>
<h3 class="text-xl font-bold mb-4">Contacto</h3>
<div class="space-y-2 text-gray-300">
<p>{{ personal.email }}</p>
<p>{{ personal.phone }}</p>
<p>{{ personal.location }}</p>
</div>
</div>
</div>
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>&copy; {{ currentYear }} {{ personal.name }}. Todos los derechos reservados.</p>
<p class="mt-2 text-sm">Hecho con y Vue.js</p>
</div>
</div>
</footer>
</template>
<script setup>
import { computed } from 'vue'
import { Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
defineProps({
personal: Object
})
const currentYear = computed(() => new Date().getFullYear())
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
twitter: Twitter,
portfolio: Globe
}
return icons[platform] || Globe
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<nav class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<div class="flex items-center space-x-2">
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
<span class="text-white font-bold text-sm">P</span>
</div>
<span class="font-bold text-gray-900 dark:text-white">Pablo de la Torre Jamardo</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<button
v-for="section in sections"
:key="section.id"
@click="$emit('navigate', section.id)"
class="text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 transition-colors font-medium"
>
{{ section.label }}
</button>
</div>
<!-- Mobile Menu Button -->
<button
@click="toggleMobileMenu"
class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
>
<Menu v-if="!isMobileMenuOpen" class="w-6 h-6" />
<X v-else class="w-6 h-6" />
</button>
</div>
<!-- Mobile Navigation -->
<div v-if="isMobileMenuOpen" class="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col space-y-2">
<button
v-for="section in sections"
:key="section.id"
@click="handleMobileNavigate(section.id)"
class="text-left px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
{{ section.label }}
</button>
</div>
</div>
</div>
</nav>
</template>
<script setup>
import { ref } from 'vue'
import { Menu, X } from 'lucide-vue-next'
defineProps({
sections: Array
})
const emit = defineEmits(['navigate'])
const isMobileMenuOpen = ref(false)
function toggleMobileMenu() {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
function handleMobileNavigate(sectionId) {
emit('navigate', sectionId)
isMobileMenuOpen.value = false
}
</script>

View File

@@ -0,0 +1,65 @@
<template>
<section id="about" class="py-20 bg-white dark:bg-gray-800">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
Sobre
</h2>
<div class="grid md:grid-cols-2 gap-12 items-center">
<!-- Bio -->
<div>
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
{{ personal.bio }}
</p>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<MapPin class="w-5 h-5 text-purple-600" />
<span class="text-gray-700 dark:text-gray-300">{{ personal.location }}</span>
</div>
<div class="flex items-center space-x-3">
<Mail class="w-5 h-5 text-purple-600" />
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
{{ personal.email }}
</a>
</div>
<div class="flex items-center space-x-3">
<Phone class="w-5 h-5 text-purple-600" />
<span class="text-gray-700 dark:text-gray-300">{{ personal.phone }}</span>
</div>
</div>
</div>
<!-- Stats -->
<div class="grid grid-cols-2 gap-6">
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">5+</div>
<div class="text-gray-600 dark:text-gray-300">Años de Experiencia</div>
</div>
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">50+</div>
<div class="text-gray-600 dark:text-gray-300">Proyectos Completados</div>
</div>
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">15+</div>
<div class="text-gray-600 dark:text-gray-300">Tecnologías</div>
</div>
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">100%</div>
<div class="text-gray-600 dark:text-gray-300">Satisfacción</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { MapPin, Mail, Phone } from 'lucide-vue-next'
defineProps({
personal: Object
})
</script>

View File

@@ -0,0 +1,170 @@
<template>
<section id="contact" class="py-20 bg-white dark:bg-gray-800">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
Contacto
</h2>
<div class="grid md:grid-cols-2 gap-12">
<!-- Contact Info -->
<div class="space-y-8">
<div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">
¡Hablemos!
</h3>
<p class="text-gray-600 dark:text-gray-300 mb-6">
Estoy siempre abierto a discutir nuevas oportunidades, proyectos interesantes o simplemente charlar sobre tecnología.
</p>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
<Mail class="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div>
<div class="font-semibold text-gray-900 dark:text-white">Email</div>
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
{{ personal.email }}
</a>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
<Phone class="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div>
<div class="font-semibold text-gray-900 dark:text-white">Teléfono</div>
<span class="text-gray-600 dark:text-gray-300">{{ personal.phone }}</span>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
<MapPin class="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div>
<div class="font-semibold text-gray-900 dark:text-white">Ubicación</div>
<span class="text-gray-600 dark:text-gray-300">{{ personal.location }}</span>
</div>
</div>
</div>
<!-- Social Links -->
<div class="flex space-x-4">
<a
v-for="(url, platform) in personal.social"
:key="platform"
:href="url"
target="_blank"
class="w-12 h-12 bg-gray-100 dark:bg-gray-700 hover:bg-purple-100 dark:hover:bg-purple-900 rounded-full flex items-center justify-center transition-colors"
>
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</a>
</div>
</div>
<!-- Contact Form -->
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl p-8">
<form @submit.prevent="handleSubmit" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Nombre
</label>
<input
v-model="form.name"
type="text"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
v-model="form.email"
type="email"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Mensaje
</label>
<textarea
v-model="form.message"
rows="4"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
></textarea>
</div>
<button
type="submit"
:disabled="isSubmitting"
class="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg font-semibold transition-colors"
>
{{ isSubmitting ? 'Enviando...' : 'Enviar Mensaje' }}
</button>
</form>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { Mail, Phone, MapPin, Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
defineProps({
personal: Object
})
const form = ref({
name: '',
email: '',
message: ''
})
const isSubmitting = ref(false)
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
twitter: Twitter,
portfolio: Globe
}
return icons[platform] || Globe
}
async function handleSubmit() {
isSubmitting.value = true
// Simulate form submission
await new Promise(resolve => setTimeout(resolve, 1000))
// Here you would typically send the form data to your backend
console.log('Form submitted:', form.value)
// Reset form
form.value = {
name: '',
email: '',
message: ''
}
isSubmitting.value = false
// Show success message (you could use a toast notification)
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<section id="experience" class="py-20 bg-gray-50 dark:bg-gray-900">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
Experiencia Profesional
</h2>
<div class="relative">
<!-- Timeline Line -->
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-purple-200 dark:bg-purple-800"></div>
<!-- Experience Items -->
<div class="space-y-12">
<div
v-for="(exp, index) in experience"
:key="exp.id"
class="relative flex items-start space-x-6"
>
<!-- Timeline Dot -->
<div class="flex-shrink-0 w-16 h-16 bg-purple-600 rounded-full flex items-center justify-center relative z-10">
<Briefcase class="w-8 h-8 text-white" />
</div>
<!-- Content -->
<div class="flex-1 bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
{{ exp.position }}
</h3>
<h4 class="text-lg text-purple-600 dark:text-purple-400 font-semibold">
{{ exp.company }}
</h4>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-2 md:mt-0">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4" />
<span>{{ exp.period }}</span>
</div>
<div class="flex items-center space-x-2 mt-1">
<MapPin class="w-4 h-4" />
<span>{{ exp.location }}</span>
</div>
</div>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-4">
{{ exp.description }}
</p>
<!-- Technologies -->
<div class="flex flex-wrap gap-2 mb-4">
<span
v-for="tech in exp.technologies"
:key="tech"
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm"
>
{{ tech }}
</span>
</div>
<!-- Achievements -->
<div class="space-y-2">
<h5 class="font-semibold text-gray-900 dark:text-white">Logros destacados:</h5>
<ul class="space-y-1">
<li
v-for="achievement in exp.achievements"
:key="achievement"
class="flex items-start space-x-2 text-gray-600 dark:text-gray-300"
>
<CheckCircle class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>{{ achievement }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { Briefcase, Calendar, MapPin, CheckCircle } from 'lucide-vue-next'
defineProps({
experience: Array
})
</script>

View File

@@ -0,0 +1,85 @@
<template>
<section class="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-900 dark:to-purple-900 pt-16">
<div class="container mx-auto px-4 text-center">
<div class="max-w-4xl mx-auto">
<!-- Avatar -->
<div class="mb-8">
<img
:src="personal.avatar"
:alt="personal.name"
class="w-32 h-32 rounded-full mx-auto shadow-2xl border-4 border-white dark:border-gray-700"
/>
</div>
<!-- Name and Title -->
<h1 class="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-4">
{{ personal.name }}
</h1>
<h2 class="text-2xl md:text-3xl text-purple-600 dark:text-purple-400 font-semibold mb-6">
{{ personal.title }}
</h2>
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
{{ personal.subtitle }}
</p>
<!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<button
@click="scrollToSection('projects')"
class="px-8 py-4 bg-purple-600 hover:bg-purple-700 text-white rounded-full font-semibold transition-all transform hover:scale-105 shadow-lg"
>
Ver Proyectos
</button>
<button
@click="scrollToSection('contact')"
class="px-8 py-4 border-2 border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-600 hover:text-white rounded-full font-semibold transition-all"
>
Contactar
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center space-x-6">
<a
v-for="(url, platform) in personal.social"
:key="platform"
:href="url"
target="_blank"
class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110"
>
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
</a>
</div>
<!-- Scroll Indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
<ChevronDown class="w-8 h-8 text-gray-400" />
</div>
</div>
</div>
</section>
</template>
<script setup>
import { Github, Linkedin, Twitter, Globe, ChevronDown } from 'lucide-vue-next'
defineProps({
personal: Object
})
function getSocialIcon(platform) {
const icons = {
github: Github,
linkedin: Linkedin,
twitter: Twitter,
portfolio: Globe
}
return icons[platform] || Globe
}
function scrollToSection(sectionId) {
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' })
}
</script>

View File

@@ -0,0 +1,99 @@
<template>
<section id="projects" class="py-20 bg-white dark:bg-gray-800">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
Proyectos Destacados
</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<div
v-for="project in projects"
:key="project.id"
class="bg-gray-50 dark:bg-gray-700 rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all transform hover:scale-105"
>
<!-- Project Image -->
<div class="relative h-48 overflow-hidden">
<img
:src="project.image"
:alt="project.title"
class="w-full h-full object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
</div>
<!-- Project Content -->
<div class="p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
{{ project.title }}
</h3>
<p class="text-gray-600 dark:text-gray-300 mb-4 line-clamp-3">
{{ project.description }}
</p>
<!-- Technologies -->
<div class="flex flex-wrap gap-2 mb-4">
<span
v-for="tech in project.technologies.slice(0, 3)"
:key="tech"
class="px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs"
>
{{ tech }}
</span>
<span
v-if="project.technologies.length > 3"
class="px-2 py-1 bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded text-xs"
>
+{{ project.technologies.length - 3 }}
</span>
</div>
<!-- Metrics -->
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
<div
v-for="(value, key) in project.metrics"
:key="key"
class="text-center p-2 bg-white dark:bg-gray-600 rounded"
>
<div class="font-bold text-purple-600 dark:text-purple-400">{{ value }}</div>
<div class="text-gray-500 dark:text-gray-400 capitalize">{{ key }}</div>
</div>
</div>
<!-- Links -->
<div class="flex space-x-2">
<a
v-if="project.links.demo"
:href="project.links.demo"
target="_blank"
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors"
>
<ExternalLink class="w-4 h-4 inline mr-1" />
Demo
</a>
<a
v-if="project.links.github"
:href="project.links.github"
target="_blank"
class="flex-1 px-4 py-2 border border-purple-600 text-purple-600 hover:bg-purple-600 hover:text-white rounded-lg text-center text-sm font-medium transition-colors"
>
<Github class="w-4 h-4 inline mr-1" />
Código
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ExternalLink, Github } from 'lucide-vue-next'
defineProps({
projects: Array
})
</script>

View File

@@ -0,0 +1,75 @@
<template>
<section id="skills" class="py-20 bg-gray-50 dark:bg-gray-900">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
Habilidades Técnicas
</h2>
<div class="grid md:grid-cols-2 gap-8">
<div
v-for="(skillGroup, category) in skills"
:key="category"
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg"
>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6 capitalize flex items-center">
<component :is="getCategoryIcon(category)" class="w-6 h-6 mr-2 text-purple-600" />
{{ getCategoryName(category) }}
</h3>
<div class="space-y-4">
<div
v-for="skill in skillGroup"
:key="skill.name"
class="space-y-2"
>
<div class="flex justify-between items-center">
<span class="font-medium text-gray-900 dark:text-white">{{ skill.name }}</span>
<div class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<span>{{ skill.years }} años</span>
<span>{{ skill.level }}%</span>
</div>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-1000"
:style="{ width: `${skill.level}%` }"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { Code, Server, Database, Cloud } from 'lucide-vue-next'
defineProps({
skills: Object
})
function getCategoryIcon(category) {
const icons = {
frontend: Code,
backend: Server,
database: Database,
devops: Cloud
}
return icons[category] || Code
}
function getCategoryName(category) {
const names = {
frontend: 'Frontend',
backend: 'Backend',
database: 'Base de Datos',
devops: 'DevOps'
}
return names[category] || category
}
</script>