Compare commits
5 Commits
0141657e14
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
088cc5aef0 | ||
| b0eb56468e | |||
| afe0c2ca34 | |||
| b5c8364b47 | |||
| 6cd1f7d620 |
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Stage 1: build
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build # Esto genera /dist
|
||||||
|
|
||||||
|
# Stage 2: serve con Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
77
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
REGISTRY_URL = "registry.nortapp.com"
|
||||||
|
USER = "andromeda"
|
||||||
|
PASS = credentials('docker-registry-password')
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Prepare Workspace & Checkout') {
|
||||||
|
steps {
|
||||||
|
echo "Cleaning workspace"
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
echo "Checking out repo..."
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Load Environment') {
|
||||||
|
steps {
|
||||||
|
echo "Loading .env secret from Jenkins..."
|
||||||
|
withCredentials([file(credentialsId: 'env', variable: 'SECRET_ENV')]) {
|
||||||
|
sh 'cp $SECRET_ENV .env'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build and Tag Docker Image') {
|
||||||
|
steps {
|
||||||
|
echo "Building Docker image..."
|
||||||
|
sh '''
|
||||||
|
make build
|
||||||
|
make tag
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('ush Docker Image') {
|
||||||
|
steps {
|
||||||
|
echo "Tagging and pushing Docker image to registry..."
|
||||||
|
sh '''
|
||||||
|
echo $PASS | docker login ${REGISTRY_URL} -u ${USER} --password-stdin
|
||||||
|
make push
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy Docker Container') {
|
||||||
|
steps {
|
||||||
|
echo "Stopping old container and running new container..."
|
||||||
|
sh '''
|
||||||
|
make stop
|
||||||
|
make run
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ Deployment completed successfully!"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "❌ Pipeline failed!"
|
||||||
|
}
|
||||||
|
always {
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Makefile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
STACK=pablotj-portfolio
|
||||||
|
APP_NAME=pablotj-portfolio-web
|
||||||
|
IMAGE_NAME=$(APP_NAME)
|
||||||
|
|
||||||
|
REGISTRY_URL=registry.nortapp.com
|
||||||
|
NAMESPACE=andromeda
|
||||||
|
TAG?=latest
|
||||||
|
|
||||||
|
IMAGE_FULL=$(REGISTRY_URL)/$(NAMESPACE)/$(IMAGE_NAME):$(TAG)
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker build -t $(IMAGE_NAME):$(TAG) .
|
||||||
|
|
||||||
|
tag:
|
||||||
|
docker tag $(IMAGE_NAME):$(TAG) $(IMAGE_FULL)
|
||||||
|
|
||||||
|
push:
|
||||||
|
docker push $(IMAGE_FULL)
|
||||||
|
|
||||||
|
run:
|
||||||
|
docker run -d \
|
||||||
|
--name $(APP_NAME) \
|
||||||
|
--label com.docker.compose.service="$(APP_NAME)" \
|
||||||
|
--label com.docker.compose.project="$(STACK)" \
|
||||||
|
--network andromeda \
|
||||||
|
--env-file .env \
|
||||||
|
$(IMAGE_FULL)
|
||||||
|
|
||||||
|
stop:
|
||||||
|
docker stop $(APP_NAME) || true
|
||||||
|
docker rm $(APP_NAME) || true
|
||||||
|
|
||||||
|
deploy: build tag push stop run
|
||||||
47
index.html
@@ -1,23 +1,38 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
10
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-loading-overlay": "^6.0.6",
|
"vue-loading-overlay": "^6.0.6",
|
||||||
"vue-preloader": "^1.1.4",
|
"vue-preloader": "^1.1.4",
|
||||||
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
"vue-typer": "^1.2.0",
|
"vue-typer": "^1.2.0",
|
||||||
"vue3-typer": "^1.0.0"
|
"vue3-typer": "^1.0.0"
|
||||||
},
|
},
|
||||||
@@ -2963,6 +2964,15 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/vue-typer": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-loading-overlay": "^6.0.6",
|
"vue-loading-overlay": "^6.0.6",
|
||||||
"vue-preloader": "^1.1.4",
|
"vue-preloader": "^1.1.4",
|
||||||
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
"vue-typer": "^1.2.0",
|
"vue-typer": "^1.2.0",
|
||||||
"vue3-typer": "^1.0.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",
|
"name": "Pablot TJ",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "avatar-bot.png",
|
"src": "images/favicon-96x96.png",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "avatar-bot.png",
|
"src": "images/web-app-manifest-192x192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "avatar-bot.png",
|
"src": "images/web-app-manifest-512x512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"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 {ref} from 'vue'
|
||||||
import {Github, Globe, Linkedin, Mail, MapPin, Phone} from 'lucide-vue-next'
|
import {Github, Globe, Linkedin, Mail, MapPin, Phone} from 'lucide-vue-next'
|
||||||
import type {Profile} from '@/domain/models/Profile'
|
import type {Profile} from '@/domain/models/Profile'
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
defineProps<{
|
defineProps<{
|
||||||
profile: Profile
|
profile: Profile
|
||||||
}>()
|
}>()
|
||||||
@@ -27,23 +28,37 @@ function getSocialIcon(platform) {
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
|
|
||||||
// Simulate form submission
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
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
|
const response = await fetch(`${import.meta.env.VITE_MAIL_API_URL}/mail`, {
|
||||||
console.log('Form submitted:', form.value)
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
// Reset form
|
if (!response.ok) {
|
||||||
form.value = {
|
throw new Error(`Error en el envío: ${response.status}`)
|
||||||
name: '',
|
}
|
||||||
email: '',
|
|
||||||
message: ''
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -117,15 +132,12 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
<!-- Contact Form -->
|
<!-- Contact Form -->
|
||||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl p-8">
|
<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">
|
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Nombre
|
Nombre
|
||||||
</label>
|
</label>
|
||||||
<input disabled
|
<input
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@@ -137,7 +149,7 @@ async function handleSubmit() {
|
|||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input disabled
|
<input
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
@@ -149,7 +161,7 @@ async function handleSubmit() {
|
|||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Mensaje
|
Mensaje
|
||||||
</label>
|
</label>
|
||||||
<textarea disabled
|
<textarea
|
||||||
v-model="form.message"
|
v-model="form.message"
|
||||||
rows="4"
|
rows="4"
|
||||||
required
|
required
|
||||||
@@ -159,7 +171,7 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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' }}
|
{{ isSubmitting ? 'Enviando...' : 'Enviar Mensaje' }}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import "./style.css"
|
|||||||
import VueTyper from 'vue3-typer'
|
import VueTyper from 'vue3-typer'
|
||||||
import "vue3-typer/dist/vue-typer.css"
|
import "vue3-typer/dist/vue-typer.css"
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
import Toast from "vue-toastification";
|
||||||
|
import "vue-toastification/dist/index.css";
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.component('VueTyper', VueTyper)
|
app.component('VueTyper', VueTyper)
|
||||||
|
|
||||||
|
app.use(Toast, {});
|
||||||
|
|
||||||
// Global error handler
|
// Global error handler
|
||||||
app.config.errorHandler = (err, vm, info) => {
|
app.config.errorHandler = (err, vm, info) => {
|
||||||
console.error("Global error:", err, info)
|
console.error("Global error:", err, info)
|
||||||
|
|||||||