Compare commits

..

11 Commits

Author SHA1 Message Date
Jose Selesan
4b82f6d05b Fix footer 2026-07-13 12:49:21 -03:00
Jose Selesan
c22e3fe918 Fixed form validation 2026-07-13 12:40:37 -03:00
Jose Selesan
401000af97 Fixed Thank you redirect 2026-07-13 12:30:30 -03:00
Jose Selesan
915c8928c8 Added contact form with email sending 2026-07-13 12:27:44 -03:00
Jose Selesan
f923dd2f56 Update logo image to a new 64x64 version 2026-06-09 14:29:13 -03:00
Jose Selesan
890975edf7 More changesd 2026-06-09 14:20:05 -03:00
Jose Selesan
5a754059d8 Fixed OpenRelay script 2026-06-09 14:18:34 -03:00
Jose Selesan
226ac46c2b Add OpenReplay tracking code for user session monitoring 2026-06-09 10:31:27 -03:00
Jose Selesan
f3c2027e44 Add pageview tracking to PostHog analytics integration 2026-06-09 10:12:22 -03:00
Jose Selesan
d604a658f9 Add PostHog analytics integration to track user interactions 2026-06-09 10:03:55 -03:00
Jose Selesan
35b8775531 Update README.md with detailed project description, stack, structure, local development instructions, and deployment guidelines 2026-06-09 09:36:51 -03:00
7 changed files with 1919 additions and 28 deletions

173
README.md
View File

@@ -1,3 +1,172 @@
# landing-2pi # 2piDev Landing Page
Repositorio para el código de la landing page de 2piDev Landing institucional de 2piDev construida con Eleventy y Tailwind CSS. El sitio es completamente estático: se genera en `_site/` y se puede servir desde Nginx, Dokploy o cualquier hosting de archivos estáticos.
## Stack
- [Eleventy](https://www.11ty.dev/) para generar HTML estático.
- [Tailwind CSS](https://tailwindcss.com/) para estilos utilitarios y componentes.
- [Bun](https://bun.sh/) como runtime/package manager.
- Nginx en producción vía Docker.
## Estructura
```txt
.
├── src/
│ ├── index.njk # Landing principal
│ ├── css/
│ │ ├── tailwind.css # CSS fuente con directivas Tailwind
│ │ └── styles.css # CSS compilado que Eleventy copia al build
│ └── images/ # Logos, screenshots, retratos y favicon
├── _site/ # Build local generado, ignorado por Git
├── .eleventy.js # Configuración de Eleventy
├── tailwind.config.js # Configuración de Tailwind
├── postcss.config.js # PostCSS + Tailwind + Autoprefixer
├── Dockerfile # Build estático + runtime Nginx
└── package.json # Scripts y dependencias
```
## Desarrollo Local
Instalar dependencias:
```bash
bun install
```
Generar el sitio:
```bash
bun run build
```
Levantar servidor de desarrollo:
```bash
bun run dev
```
Por defecto, Eleventy sirve el sitio en `http://localhost:8080/`.
## Scripts
```json
{
"build:css": "tailwindcss -i ./src/css/tailwind.css -o ./src/css/styles.css --minify",
"build": "bun run build:css && eleventy",
"dev": "bun run build:css && eleventy --serve --watch"
}
```
Notas:
- `src/css/tailwind.css` es el archivo que se edita.
- `src/css/styles.css` es generado por Tailwind y se mantiene versionado para que Eleventy pueda copiarlo como asset estático.
- `_site/` no se versiona.
## Deploy con Dokploy
La forma recomendada para Dokploy es usar el `Dockerfile` incluido. El contenedor:
1. Instala dependencias con Bun.
2. Ejecuta `bun run build`.
3. Copia `_site/` a Nginx.
4. Sirve la landing desde el puerto `80`.
En Dokploy:
- Tipo de deploy: Dockerfile.
- Puerto interno: `80`.
- No requiere variables de entorno.
- No requiere base de datos ni servicios externos.
Build manual de la imagen:
```bash
docker build -t 2pidev-landing-page .
```
Ejecución local con Docker:
```bash
docker run --rm -p 8080:80 2pidev-landing-page
```
Luego abrir `http://localhost:8080/`.
## Assets
Los assets viven en `src/images/` y se copian automáticamente a `_site/images/`.
Incluye:
- Logo principal y favicon.
- Imágenes generadas/originales para hero, resultados y casos de éxito.
- Retratos ficticios generados para testimonios.
- Logos SVG de tecnologías: `.NET`, TypeScript, React, PostgreSQL y AWS.
Para reemplazar una imagen, conservar el mismo nombre de archivo evita cambios en el HTML. Si se cambia el nombre, actualizar la referencia en `src/index.njk`.
## Contenido Principal
Toda la landing está en `src/index.njk`. Las secciones actuales son:
- Header con navegación y CTA.
- Hero principal.
- Problemas frecuentes de empresas en crecimiento.
- Soluciones digitales.
- Resultados y métricas.
- Método de trabajo.
- Stack tecnológico.
- Casos de éxito.
- Testimonios.
- Formulario de contacto.
- Footer.
Algunas listas, como servicios, pasos, tecnologías, casos y testimonios, están definidas como arrays dentro del template para facilitar edición rápida.
## Convenciones
- Mantener textos visibles con tildes y ortografía en español.
- Mantener assets locales siempre que sea posible para evitar dependencias externas.
- Usar SVG para logos e íconos de marca cuando exista una fuente confiable.
- Antes de commitear, correr:
```bash
bun run build
```
## Git
Repositorio remoto:
```bash
git remote -v
```
Debe apuntar a:
```txt
https://git.2pidev.com/jselesan/landing-2pi.git
```
Flujo sugerido:
```bash
git status
bun run build
git add .
git commit -m "Describe el cambio"
git push
```
## Mantenimiento
Si se agregan nuevas tecnologías, sumar el SVG a `src/images/` y actualizar el array `techs` en `src/index.njk`.
Si se agregan nuevas secciones o componentes visuales, preferir mantenerlos en el mismo template mientras la landing siga siendo simple. Crear layouts/includes de Eleventy solo cuando haya repetición real o más páginas.
## Licencias y Uso de Imágenes
Los retratos de testimonios son ficticios y generados para este proyecto. Los logos de tecnologías pertenecen a sus respectivas marcas. Las imágenes de producto/interfaz usadas como visuales de la landing deben mantenerse como assets locales del proyecto para que el deploy sea reproducible.

1550
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
src/_data/site.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
posthogKey: process.env.POSTHOG_API_KEY || null,
posthogHost: process.env.POSTHOG_API_HOST || 'https://us.i.posthog.com',
}

File diff suppressed because one or more lines are too long

30
src/gracias.njk Normal file
View File

@@ -0,0 +1,30 @@
---
layout: false
title: Gracias | 2piDev
---
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body class="min-h-screen bg-gradient-to-br from-orange-50 via-white to-blue-50">
<div class="flex min-h-screen items-center justify-center px-4">
<div class="max-w-md text-center">
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-green-100">
<svg class="h-10 w-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h1 class="font-display text-4xl font-bold text-slate-900">¡Gracias!</h1>
<p class="mt-4 text-lg text-slate-600">Hemos recibido tu mensaje y te contactaremos pronto.</p>
<a href="/" class="mt-8 inline-flex items-center gap-2 rounded-lg bg-orange-600 px-6 py-3 text-sm font-bold text-white hover:bg-orange-700 transition-colors">
← Volver al inicio
</a>
</div>
</div>
</body>
</html>

BIN
src/images/logo-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -12,12 +12,50 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
<title>{{ title }}</title> <title>{{ title }}</title>
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/styles.css"> <link rel="stylesheet" href="/css/styles.css">
{% if site.posthogKey %}
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.full.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getSurveys getActiveMatchingSurveys captureException".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('{{ site.posthogKey }}', {
api_host: '{{ site.posthogHost }}',
capture_performance: true,
disable_session_recording: false,
})
posthog.capture('$pageview');
</script>
{% endif %}
<!-- OpenReplay Tracking Code -->
<script>
var initOpts = {
projectKey: "5o5HLGZhn4TlGBbOFkVu",
// 0 - plain, 1 - obscured, 2 - ignored
defaultInputMode: 2,
obscureTextNumbers: false,
obscureTextEmails: true,
};
var startOpts = { userID: "" };
(function(A,s,a,y,e,r){
r=window.OpenReplay=[e,r,y,[s-1, e]];
s=document.createElement('script');s.src=A;s.async=!a;
document.getElementsByTagName('head')[0].appendChild(s);
r.start=function(v){r.push([0])};
r.stop=function(v){r.push([1])};
r.setUserID=function(id){r.push([2,id])};
r.setUserAnonymousID=function(id){r.push([3,id])};
r.setMetadata=function(k,v){r.push([4,k,v])};
r.event=function(k,p,i){r.push([5,k,p,i])};
r.issue=function(k,p){r.push([6,k,p])};
r.isActive=function(){return false};
r.getSessionToken=function(){};
})("//static.openreplay.com/latest/openreplay.js",1,0,initOpts,startOpts);
</script>
</head> </head>
<body> <body>
<header class="sticky top-0 z-50 border-b border-line/70 bg-white/92 backdrop-blur"> <header class="sticky top-0 z-50 border-b border-line/70 bg-white/92 backdrop-blur">
<nav class="section-shell flex h-16 items-center justify-between gap-5"> <nav class="section-shell flex h-16 items-center justify-between gap-5">
<a href="#inicio" class="flex items-center gap-3" aria-label="2piDev inicio"> <a href="#inicio" class="flex items-center gap-3" aria-label="2piDev inicio">
<img src="/images/logo.svg" alt="" class="h-10 w-10"> <img src="/images/logo-64x64.png" alt="" class="h-10 w-10">
<span class="font-display text-xl font-bold text-ink">2piDev</span> <span class="font-display text-xl font-bold text-ink">2piDev</span>
</a> </a>
<div class="hidden items-center gap-8 md:flex"> <div class="hidden items-center gap-8 md:flex">
@@ -284,20 +322,25 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
<p class="flex items-center gap-3"><span class="h-2.5 w-2.5 rounded-full bg-white"></span>Sede Central, Hub de Innovación Digital</p> <p class="flex items-center gap-3"><span class="h-2.5 w-2.5 rounded-full bg-white"></span>Sede Central, Hub de Innovación Digital</p>
</div> </div>
</div> </div>
<form class="rounded-xl border border-white/20 bg-white p-6 text-ink shadow-panel"> <form action="https://formsubmit.co/hola@2pidev.com" method="POST" class="rounded-xl border border-white/20 bg-white p-6 text-ink shadow-panel">
<input type="hidden" name="_next" value="https://www.2pidev.com/gracias">
<input type="hidden" name="_captcha" value="false">
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
<label class="text-sm font-bold">Nombre <label class="text-sm font-bold">Nombre
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="nombre" placeholder="Juan Pérez"> <input id="nombre" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="nombre" placeholder="Juan Pérez">
<p class="error-message hidden mt-1 text-xs text-red-500">Por favor completá tu nombre</p>
</label> </label>
<label class="text-sm font-bold">Empresa <label class="text-sm font-bold">Empresa
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="empresa" placeholder="Nombre de tu empresa"> <input id="empresa" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="empresa" placeholder="Nombre de tu empresa">
</label> </label>
</div> </div>
<label class="mt-4 block text-sm font-bold">Email corporativo <label class="mt-4 block text-sm font-bold">Email corporativo
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="email" name="email" placeholder="juan@empresa.com"> <input id="email" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="email" placeholder="juan@empresa.com">
<p class="error-message hidden mt-1 text-xs text-red-500">Ingresá un email válido</p>
</label> </label>
<label class="mt-4 block text-sm font-bold">Mensaje <label class="mt-4 block text-sm font-bold">Mensaje
<textarea class="mt-2 min-h-32 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" name="mensaje" placeholder="Cuéntanos un poco sobre tu proyecto..."></textarea> <textarea id="mensaje" class="field mt-2 min-h-32 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" name="mensaje" placeholder="Cuéntanos un poco sobre tu proyecto..."></textarea>
<p class="error-message hidden mt-1 text-xs text-red-500">Por favor completá tu mensaje</p>
</label> </label>
<button class="btn-primary mt-5 w-full" type="submit">Enviar mensaje</button> <button class="btn-primary mt-5 w-full" type="submit">Enviar mensaje</button>
</form> </form>
@@ -306,7 +349,7 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
</main> </main>
<footer class="bg-white py-12"> <footer class="bg-white py-12">
<div class="section-shell grid gap-8 md:grid-cols-4"> <div class="section-shell grid gap-8 md:grid-cols-[auto_1fr]">
<div> <div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img src="/images/logo.svg" alt="" class="h-10 w-10"> <img src="/images/logo.svg" alt="" class="h-10 w-10">
@@ -314,23 +357,7 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
</div> </div>
<p class="mt-4 text-sm leading-6 text-muted">Engineering elegance. Software de alto rendimiento diseñado para el futuro corporativo.</p> <p class="mt-4 text-sm leading-6 text-muted">Engineering elegance. Software de alto rendimiento diseñado para el futuro corporativo.</p>
</div> </div>
<div> <div class="md:text-right">
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Compañía</h3>
<div class="mt-4 space-y-2 text-sm text-muted">
<p>Sobre nosotros</p>
<p>Carreras</p>
<p>Blog tech</p>
</div>
</div>
<div>
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Legal</h3>
<div class="mt-4 space-y-2 text-sm text-muted">
<p>Privacidad</p>
<p>Términos</p>
<p>Seguridad</p>
</div>
</div>
<div>
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Contacto</h3> <h3 class="text-sm font-black uppercase tracking-wide text-ink">Contacto</h3>
<div class="mt-4 space-y-2 text-sm text-muted"> <div class="mt-4 space-y-2 text-sm text-muted">
<p>Hablemos de tu proyecto</p> <p>Hablemos de tu proyecto</p>
@@ -342,5 +369,116 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
<p>(c) 2026 2piDev. Engineered Elegance.</p> <p>(c) 2026 2piDev. Engineered Elegance.</p>
</div> </div>
</footer> </footer>
</body> {% if site.posthogKey %}
<script>
(function() {
document.addEventListener('click', function(e) {
var link = e.target.closest('a');
if (!link) return;
if (link.matches('.btn-primary, .btn-secondary, .nav-link')) {
posthog.capture('cta_click', {
label: link.textContent.trim(),
href: link.getAttribute('href')
});
}
});
var form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function() {
var data = new FormData(form);
posthog.capture('contact_form_submitted', {
nombre: data.get('nombre') || '',
empresa: data.get('empresa') || '',
email: data.get('email') || ''
});
});
}
var sections = ['servicios', 'soluciones', 'tecnologias', 'contacto', 'casos'];
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
posthog.capture('section_view', { section: entry.target.id });
}
});
}, { threshold: 0.3 });
sections.forEach(function(id) {
var el = document.getElementById(id);
if (el) observer.observe(el);
});
})();
</script>
{% endif %}
<script>
(function() {
var form = document.querySelector('form');
if (!form) return;
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var fields = {
nombre: { el: document.getElementById('nombre'), msg: 'Por favor completá tu nombre' },
email: { el: document.getElementById('email'), msg: 'Ingresá un email válido' },
mensaje: { el: document.getElementById('mensaje'), msg: 'Por favor completá tu mensaje' }
};
function showError(field, message) {
field.el.classList.add('border-red-500');
field.el.classList.remove('border-line');
var errorEl = field.el.parentElement.querySelector('.error-message');
if (errorEl) {
errorEl.textContent = message || field.msg;
errorEl.classList.remove('hidden');
}
}
function clearError(field) {
field.el.classList.remove('border-red-500');
field.el.classList.add('border-line');
var errorEl = field.el.parentElement.querySelector('.error-message');
if (errorEl) errorEl.classList.add('hidden');
}
function validate() {
var valid = true;
if (!fields.nombre.el.value.trim()) {
showError(fields.nombre);
valid = false;
} else {
clearError(fields.nombre);
}
if (!fields.email.el.value.trim() || !emailRegex.test(fields.email.el.value.trim())) {
showError(fields.email);
valid = false;
} else {
clearError(fields.email);
}
if (!fields.mensaje.el.value.trim()) {
showError(fields.mensaje);
valid = false;
} else {
clearError(fields.mensaje);
}
return valid;
}
form.addEventListener('submit', function(e) {
if (!validate()) {
e.preventDefault();
}
});
Object.keys(fields).forEach(function(key) {
fields[key].el.addEventListener('input', function() {
clearError(fields[key]);
});
});
})();
</script>
</body>
</html> </html>