Compare commits
2 Commits
0141657e14
...
b5c8364b47
Author | SHA1 | Date | |
---|---|---|---|
b5c8364b47 | |||
6cd1f7d620 |
47
index.html
@ -1,23 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link href="/avatar-bot.png" rel="icon" type="image/png"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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 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 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"/>
|
||||
|
||||
<title>Portfolio | Pablot TJ</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>Portfolio | Pablo de la Torre Jamardo (Pablo TJ)</title>
|
||||
|
||||
<!-- SEO -->
|
||||
<meta name="description" content="Portfolio personal de Pablo de la Torre Jamardo (Pablo TJ): proyectos, desarrollo software, programación y soluciones tecnológicas." />
|
||||
<meta name="keywords" content="portfolio, Pablo TJ, Pablo de la Torre, desarrollador, programación, software, tecnología" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="author" content="Pablo de la Torre Jamardo" />
|
||||
<link rel="canonical" href="https://pablotj.com/" />
|
||||
|
||||
<!-- PWA / Mobile -->
|
||||
<meta name="theme-color" content="#062342" />
|
||||
<meta name="apple-mobile-web-app-title" content="Pablo TJ" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
|
||||
<!-- Favicon & App Icons -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/images/site.webmanifest" />
|
||||
<link rel="shortcut icon" href="/images/favicon.ico" />
|
||||
|
||||
<!-- Open Graph (Facebook, LinkedIn, WhatsApp) -->
|
||||
<meta property="og:title" content="Portfolio | Pablo de la Torre Jamardo (Pablo TJ)" />
|
||||
<meta property="og:description" content="Explora el portfolio de Pablo TJ: proyectos de software, programación y desarrollo tecnológico." />
|
||||
<meta property="og:image" content="https://pablotj.com/images/favicon.svg" />
|
||||
<meta property="og:url" content="https://pablotj.com/" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
10
package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"vue": "^3.4.21",
|
||||
"vue-loading-overlay": "^6.0.6",
|
||||
"vue-preloader": "^1.1.4",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vue-typer": "^1.2.0",
|
||||
"vue3-typer": "^1.0.0"
|
||||
},
|
||||
@ -2963,6 +2964,15 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-toastification": {
|
||||
"version": "2.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-typer": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
|
||||
|
@ -30,6 +30,7 @@
|
||||
"vue": "^3.4.21",
|
||||
"vue-loading-overlay": "^6.0.6",
|
||||
"vue-preloader": "^1.1.4",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vue-typer": "^1.2.0",
|
||||
"vue3-typer": "^1.0.0"
|
||||
},
|
||||
|
Before Width: | Height: | Size: 372 KiB |
BIN
public/images/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
public/images/favicon-96x96.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
public/images/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
public/images/favicon.svg
Normal file
After Width: | Height: | Size: 496 KiB |
21
public/images/site.webmanifest
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Pablo TJ",
|
||||
"short_name": "Pablo TJ",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.ico/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.ico/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
public/images/web-app-manifest-192x192.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
public/images/web-app-manifest-512x512.png
Normal file
After Width: | Height: | Size: 476 KiB |
@ -3,17 +3,17 @@
|
||||
"name": "Pablot TJ",
|
||||
"icons": [
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"src": "images/favicon-96x96.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"src": "images/web-app-manifest-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "avatar-bot.png",
|
||||
"src": "images/web-app-manifest-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
|
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -2,7 +2,8 @@
|
||||
import {ref} from 'vue'
|
||||
import {Github, Globe, Linkedin, Mail, MapPin, Phone} from 'lucide-vue-next'
|
||||
import type {Profile} from '@/domain/models/Profile'
|
||||
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
defineProps<{
|
||||
profile: Profile
|
||||
}>()
|
||||
@ -27,23 +28,37 @@ function getSocialIcon(platform) {
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
try {
|
||||
const payload = {
|
||||
from: form.value.email,
|
||||
subject: form.value.name,
|
||||
body: form.value.message
|
||||
}
|
||||
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log('Form submitted:', form.value)
|
||||
const response = await fetch(`${import.meta.env.VITE_MAIL_API_URL}/mail`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
// Reset form
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error en el envío: ${response.status}`)
|
||||
}
|
||||
|
||||
form.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
toast.success("✅ ¡Mensaje enviado correctamente! Te responderé pronto.")
|
||||
} catch (error) {
|
||||
toast.error("❌ Hubo un error al enviar el mensaje. Inténtalo de nuevo.")
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
||||
isSubmitting.value = false
|
||||
|
||||
// Show success message (you could use a toast notification)
|
||||
alert('¡Mensaje enviado correctamente! Te responderé pronto.')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -117,15 +132,12 @@ async function handleSubmit() {
|
||||
|
||||
<!-- 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 disabled
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
@ -137,7 +149,7 @@ async function handleSubmit() {
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input disabled
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
@ -149,7 +161,7 @@ async function handleSubmit() {
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea disabled
|
||||
<textarea
|
||||
v-model="form.message"
|
||||
rows="4"
|
||||
required
|
||||
@ -159,7 +171,7 @@ async function handleSubmit() {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting || 1===1"
|
||||
: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' }}
|
||||
|
@ -3,10 +3,14 @@ import "./style.css"
|
||||
import VueTyper from 'vue3-typer'
|
||||
import "vue3-typer/dist/vue-typer.css"
|
||||
import App from "./App.vue";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
|
||||
const app = createApp(App)
|
||||
app.component('VueTyper', VueTyper)
|
||||
|
||||
app.use(Toast, {});
|
||||
|
||||
// Global error handler
|
||||
app.config.errorHandler = (err, vm, info) => {
|
||||
console.error("Global error:", err, info)
|
||||
|