Initial functional version of the portfolio chatbot site
This commit is contained in:
21
src/components/chat/ChatFloatingButton.vue
Normal file
21
src/components/chat/ChatFloatingButton.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="fixed bottom-6 right-6 z-50">
|
||||
<button
|
||||
@click="$emit('toggle-chat')"
|
||||
class="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110 flex items-center justify-center"
|
||||
>
|
||||
<MessageCircle class="w-8 h-8" />
|
||||
</button>
|
||||
|
||||
<!-- Notification Badge -->
|
||||
<div class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs rounded-full flex items-center justify-center animate-pulse">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MessageCircle } from 'lucide-vue-next'
|
||||
|
||||
defineEmits(['toggle-chat'])
|
||||
</script>
|
||||
148
src/components/chat/ChatPopup.vue
Normal file
148
src/components/chat/ChatPopup.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 z-50 flex items-end justify-end p-4">
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
||||
@click="$emit('close')"
|
||||
></div>
|
||||
|
||||
<!-- Chat Window -->
|
||||
<div class="relative w-full max-w-md h-96 bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img
|
||||
:src="avatarBot"
|
||||
alt="Assistant"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Asistente Virtual</h3>
|
||||
<p class="text-xs text-green-500">En línea</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" ref="messagesContainer">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="flex items-start space-x-2"
|
||||
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
||||
>
|
||||
<img
|
||||
:src="message.role === 'user' ? avatarUser : avatarBot"
|
||||
alt="avatar"
|
||||
class="w-6 h-6 rounded-full flex-shrink-0"
|
||||
/>
|
||||
<div
|
||||
class="max-w-xs px-3 py-2 rounded-lg text-sm"
|
||||
:class="message.role === 'user'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'"
|
||||
>
|
||||
<div v-if="message.typing" class="flex space-x-1">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
||||
</div>
|
||||
<div v-else v-html="message.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div v-if="showQuickActions" class="px-4 pb-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="action in chatConfig.welcome.quickActions"
|
||||
:key="action.text"
|
||||
@click="sendMessage(action.text)"
|
||||
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-xs hover:bg-purple-200 dark:hover:bg-purple-800 transition-colors"
|
||||
>
|
||||
{{ action.text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="input"
|
||||
@keydown.enter="handleSubmit"
|
||||
:disabled="isLoading"
|
||||
placeholder="Escribe tu pregunta..."
|
||||
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
|
||||
/>
|
||||
<button
|
||||
@click="handleSubmit"
|
||||
:disabled="isLoading || !input.trim()"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Send class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||||
import { X, Send } from 'lucide-vue-next'
|
||||
import { useChatService } from '@/services/ChatService'
|
||||
import avatarUser from '@/assets/avatar-user.jpg'
|
||||
import avatarBot from '@/assets/avatar-bot.png'
|
||||
|
||||
const props = defineProps({
|
||||
chatConfig: Object
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const { messages, input, isLoading, sendMessage: sendChatMessage } = useChatService(props.chatConfig)
|
||||
const messagesContainer = ref(null)
|
||||
|
||||
const showQuickActions = computed(() => {
|
||||
return messages.value.length <= 1 && props.chatConfig?.welcome?.quickActions
|
||||
})
|
||||
|
||||
function sendMessage(text) {
|
||||
sendChatMessage(text)
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (input.value.trim()) {
|
||||
sendMessage(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Send welcome message
|
||||
if (props.chatConfig?.welcome?.message) {
|
||||
setTimeout(() => {
|
||||
messages.value.push({
|
||||
id: Date.now(),
|
||||
role: 'assistant',
|
||||
content: props.chatConfig.welcome.message
|
||||
})
|
||||
nextTick(() => scrollToBottom())
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
74
src/components/layout/AppFooter.vue
Normal file
74
src/components/layout/AppFooter.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<footer class="bg-gray-900 text-white py-12">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
<!-- About -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">{{ personal.name }}</h3>
|
||||
<p class="text-gray-300 mb-4">
|
||||
{{ personal.title }} especializado en crear experiencias web excepcionales.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-5 h-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Enlaces Rápidos</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#about" class="text-gray-300 hover:text-white transition-colors">Sobre mí</a></li>
|
||||
<li><a href="#experience" class="text-gray-300 hover:text-white transition-colors">Experiencia</a></li>
|
||||
<li><a href="#projects" class="text-gray-300 hover:text-white transition-colors">Proyectos</a></li>
|
||||
<li><a href="#skills" class="text-gray-300 hover:text-white transition-colors">Habilidades</a></li>
|
||||
<li><a href="#contact" class="text-gray-300 hover:text-white transition-colors">Contacto</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-4">Contacto</h3>
|
||||
<div class="space-y-2 text-gray-300">
|
||||
<p>{{ personal.email }}</p>
|
||||
<p>{{ personal.phone }}</p>
|
||||
<p>{{ personal.location }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© {{ currentYear }} {{ personal.name }}. Todos los derechos reservados.</p>
|
||||
<p class="mt-2 text-sm">Hecho con ❤️ y Vue.js</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
</script>
|
||||
72
src/components/layout/AppNavigation.vue
Normal file
72
src/components/layout/AppNavigation.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<nav class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-bold text-sm">P</span>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900 dark:text-white">Pablo de la Torre Jamardo</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="$emit('navigate', section.id)"
|
||||
class="text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 transition-colors font-medium"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
@click="toggleMobileMenu"
|
||||
class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<Menu v-if="!isMobileMenuOpen" class="w-6 h-6" />
|
||||
<X v-else class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div v-if="isMobileMenuOpen" class="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
@click="handleMobileNavigate(section.id)"
|
||||
class="text-left px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
{{ section.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Menu, X } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
sections: Array
|
||||
})
|
||||
|
||||
const emit = defineEmits(['navigate'])
|
||||
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
function handleMobileNavigate(sectionId) {
|
||||
emit('navigate', sectionId)
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
65
src/components/sections/AboutSection.vue
Normal file
65
src/components/sections/AboutSection.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<section id="about" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Sobre mí
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
||||
<!-- Bio -->
|
||||
<div>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||
{{ personal.bio }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<MapPin class="w-5 h-5 text-purple-600" />
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ personal.location }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Mail class="w-5 h-5 text-purple-600" />
|
||||
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Phone class="w-5 h-5 text-purple-600" />
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ personal.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">5+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Años de Experiencia</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">50+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Proyectos Completados</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">15+</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Tecnologías</div>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-purple-50 dark:bg-gray-700 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">100%</div>
|
||||
<div class="text-gray-600 dark:text-gray-300">Satisfacción</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MapPin, Mail, Phone } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
</script>
|
||||
170
src/components/sections/ContactSection.vue
Normal file
170
src/components/sections/ContactSection.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<section id="contact" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Contacto
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-12">
|
||||
<!-- Contact Info -->
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
¡Hablemos!
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Estoy siempre abierto a discutir nuevas oportunidades, proyectos interesantes o simplemente charlar sobre tecnología.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<Mail class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Email</div>
|
||||
<a :href="`mailto:${personal.email}`" class="text-purple-600 hover:underline">
|
||||
{{ personal.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<Phone class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Teléfono</div>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ personal.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
|
||||
<MapPin class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">Ubicación</div>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ personal.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="w-12 h-12 bg-gray-100 dark:bg-gray-700 hover:bg-purple-100 dark:hover:bg-purple-900 rounded-full flex items-center justify-center transition-colors"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl p-8">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea
|
||||
v-model="form.message"
|
||||
rows="4"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
{{ isSubmitting ? 'Enviando...' : 'Enviar Mensaje' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Mail, Phone, MapPin, Github, Linkedin, Twitter, Globe } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log('Form submitted:', form.value)
|
||||
|
||||
// Reset form
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
isSubmitting.value = false
|
||||
|
||||
// Show success message (you could use a toast notification)
|
||||
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
|
||||
}
|
||||
</script>
|
||||
92
src/components/sections/ExperienceSection.vue
Normal file
92
src/components/sections/ExperienceSection.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<section id="experience" class="py-20 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Experiencia Profesional
|
||||
</h2>
|
||||
|
||||
<div class="relative">
|
||||
<!-- Timeline Line -->
|
||||
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-purple-200 dark:bg-purple-800"></div>
|
||||
|
||||
<!-- Experience Items -->
|
||||
<div class="space-y-12">
|
||||
<div
|
||||
v-for="(exp, index) in experience"
|
||||
:key="exp.id"
|
||||
class="relative flex items-start space-x-6"
|
||||
>
|
||||
<!-- Timeline Dot -->
|
||||
<div class="flex-shrink-0 w-16 h-16 bg-purple-600 rounded-full flex items-center justify-center relative z-10">
|
||||
<Briefcase class="w-8 h-8 text-white" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{{ exp.position }}
|
||||
</h3>
|
||||
<h4 class="text-lg text-purple-600 dark:text-purple-400 font-semibold">
|
||||
{{ exp.company }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mt-2 md:mt-0">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Calendar class="w-4 h-4" />
|
||||
<span>{{ exp.period }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mt-1">
|
||||
<MapPin class="w-4 h-4" />
|
||||
<span>{{ exp.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{{ exp.description }}
|
||||
</p>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tech in exp.technologies"
|
||||
:key="tech"
|
||||
class="px-3 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded-full text-sm"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Achievements -->
|
||||
<div class="space-y-2">
|
||||
<h5 class="font-semibold text-gray-900 dark:text-white">Logros destacados:</h5>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="achievement in exp.achievements"
|
||||
:key="achievement"
|
||||
class="flex items-start space-x-2 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<CheckCircle class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{{ achievement }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Briefcase, Calendar, MapPin, CheckCircle } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
experience: Array
|
||||
})
|
||||
</script>
|
||||
85
src/components/sections/HeroSection.vue
Normal file
85
src/components/sections/HeroSection.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<section class="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-900 dark:to-purple-900 pt-16">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Avatar -->
|
||||
<div class="mb-8">
|
||||
<img
|
||||
:src="personal.avatar"
|
||||
:alt="personal.name"
|
||||
class="w-32 h-32 rounded-full mx-auto shadow-2xl border-4 border-white dark:border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name and Title -->
|
||||
<h1 class="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ personal.name }}
|
||||
</h1>
|
||||
|
||||
<h2 class="text-2xl md:text-3xl text-purple-600 dark:text-purple-400 font-semibold mb-6">
|
||||
{{ personal.title }}
|
||||
</h2>
|
||||
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
|
||||
{{ personal.subtitle }}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
||||
<button
|
||||
@click="scrollToSection('projects')"
|
||||
class="px-8 py-4 bg-purple-600 hover:bg-purple-700 text-white rounded-full font-semibold transition-all transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Ver Proyectos
|
||||
</button>
|
||||
<button
|
||||
@click="scrollToSection('contact')"
|
||||
class="px-8 py-4 border-2 border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-600 hover:text-white rounded-full font-semibold transition-all"
|
||||
>
|
||||
Contactar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="flex justify-center space-x-6">
|
||||
<a
|
||||
v-for="(url, platform) in personal.social"
|
||||
:key="platform"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all transform hover:scale-110"
|
||||
>
|
||||
<component :is="getSocialIcon(platform)" class="w-6 h-6 text-gray-600 dark:text-gray-300" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<ChevronDown class="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Github, Linkedin, Twitter, Globe, ChevronDown } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
personal: Object
|
||||
})
|
||||
|
||||
function getSocialIcon(platform) {
|
||||
const icons = {
|
||||
github: Github,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
portfolio: Globe
|
||||
}
|
||||
return icons[platform] || Globe
|
||||
}
|
||||
|
||||
function scrollToSection(sectionId) {
|
||||
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
99
src/components/sections/ProjectsSection.vue
Normal file
99
src/components/sections/ProjectsSection.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<section id="projects" class="py-20 bg-white dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Proyectos Destacados
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
class="bg-gray-50 dark:bg-gray-700 rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all transform hover:scale-105"
|
||||
>
|
||||
<!-- Project Image -->
|
||||
<div class="relative h-48 overflow-hidden">
|
||||
<img
|
||||
:src="project.image"
|
||||
:alt="project.title"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Project Content -->
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ project.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4 line-clamp-3">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tech in project.technologies.slice(0, 3)"
|
||||
:key="tech"
|
||||
class="px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs"
|
||||
>
|
||||
{{ tech }}
|
||||
</span>
|
||||
<span
|
||||
v-if="project.technologies.length > 3"
|
||||
class="px-2 py-1 bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded text-xs"
|
||||
>
|
||||
+{{ project.technologies.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
|
||||
<div
|
||||
v-for="(value, key) in project.metrics"
|
||||
:key="key"
|
||||
class="text-center p-2 bg-white dark:bg-gray-600 rounded"
|
||||
>
|
||||
<div class="font-bold text-purple-600 dark:text-purple-400">{{ value }}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 capitalize">{{ key }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="flex space-x-2">
|
||||
<a
|
||||
v-if="project.links.demo"
|
||||
:href="project.links.demo"
|
||||
target="_blank"
|
||||
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink class="w-4 h-4 inline mr-1" />
|
||||
Demo
|
||||
</a>
|
||||
<a
|
||||
v-if="project.links.github"
|
||||
:href="project.links.github"
|
||||
target="_blank"
|
||||
class="flex-1 px-4 py-2 border border-purple-600 text-purple-600 hover:bg-purple-600 hover:text-white rounded-lg text-center text-sm font-medium transition-colors"
|
||||
>
|
||||
<Github class="w-4 h-4 inline mr-1" />
|
||||
Código
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ExternalLink, Github } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
projects: Array
|
||||
})
|
||||
</script>
|
||||
75
src/components/sections/SkillsSection.vue
Normal file
75
src/components/sections/SkillsSection.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<section id="skills" class="py-20 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
||||
Habilidades Técnicas
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div
|
||||
v-for="(skillGroup, category) in skills"
|
||||
:key="category"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg"
|
||||
>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6 capitalize flex items-center">
|
||||
<component :is="getCategoryIcon(category)" class="w-6 h-6 mr-2 text-purple-600" />
|
||||
{{ getCategoryName(category) }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="skill in skillGroup"
|
||||
:key="skill.name"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ skill.name }}</span>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>{{ skill.years }} años</span>
|
||||
<span>{{ skill.level }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-1000"
|
||||
:style="{ width: `${skill.level}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Code, Server, Database, Cloud } from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
skills: Object
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
const icons = {
|
||||
frontend: Code,
|
||||
backend: Server,
|
||||
database: Database,
|
||||
devops: Cloud
|
||||
}
|
||||
return icons[category] || Code
|
||||
}
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
frontend: 'Frontend',
|
||||
backend: 'Backend',
|
||||
database: 'Base de Datos',
|
||||
devops: 'DevOps'
|
||||
}
|
||||
return names[category] || category
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user