diff --git a/.gitignore b/.gitignore index 3fef480..d7c5ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,58 @@ -backend/wordpress/wp-config-local.php +# ── Secretos y entorno ────────────────────────────────────── +.env +.env.* +!.env.example +.token_gitea +LOCAL_DEPLOY_CONTEXT.md + +# ── WordPress core (no versionar — viene de wordpress.org) ── +backend/wordpress/wp-admin/ +backend/wordpress/wp-includes/ +backend/wordpress/wp-activate.php +backend/wordpress/wp-blog-header.php +backend/wordpress/wp-comments-post.php +backend/wordpress/wp-cron.php +backend/wordpress/wp-links-opml.php +backend/wordpress/wp-load.php +backend/wordpress/wp-login.php +backend/wordpress/wp-mail.php +backend/wordpress/wp-settings.php +backend/wordpress/wp-signup.php +backend/wordpress/wp-trackback.php +backend/wordpress/xmlrpc.php +backend/wordpress/index.php +backend/wordpress/readme.html +backend/wordpress/license.txt + +# ── wp-content: solo versionar lo propio ──────────────────── backend/wordpress/wp-content/uploads/ backend/wordpress/wp-content/cache/ backend/wordpress/wp-content/upgrade/ backend/wordpress/wp-content/backups/ -.env -.token_gitea +backend/wordpress/wp-content/debug.log +backend/wordpress/wp-content/languages/ + +# Temas por defecto de WordPress (no son nuestros) +backend/wordpress/wp-content/themes/twentytwentythree/ +backend/wordpress/wp-content/themes/twentytwentyfour/ +backend/wordpress/wp-content/themes/twentytwentyfive/ + +# Plugins descargados de WordPress.org (no son nuestros) +backend/wordpress/wp-content/plugins/cmb2/ +backend/wordpress/wp-content/plugins/wp-mail-smtp/ +backend/wordpress/wp-content/plugins/advanced-custom-fields/ +backend/wordpress/wp-content/plugins/index.php + +# ── Sistema / editor ──────────────────────────────────────── .DS_Store +Thumbs.db +.idea/ +.vscode/* +!.vscode/extensions.json +*.swp +*.swo +.claude/ + +# ── Archivos temporales ────────────────────────────────────── +/tmp/ +*.tmp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e336ee8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog — pflores.linkedcorp.net + +Formato: `X.Y.Z` según `VERSIONADO.md` + +--- + +## [1.0.0] — 2026-06-05 + +### Lanzamiento inicial completo + +**Temas WordPress (4)** +- `persona-cv` — Personage/Pixflow style: hero fullscreen, secciones alternas, timeline centrado, barras de skills, Raleway + Open Sans. Tema activo. +- `lumina` — Diseño claro alegre, paleta teal+ámbar, sidebar izquierdo, foto circular, Plus Jakarta Sans. +- `apex-cv` — Dark theme serio, sidebar fijo izquierdo, acento cian eléctrico, Space Grotesk. +- `cvolio` — Glassmorphism oscuro, paleta café/tostado, sidebar derecho. + +**Gestión de contenido** +- Plugin propio `pflores-cv-fields` con CMB2 (100 % gratuito). +- Panel admin **"CV Patricia Flores"** con 5 sub-páginas editables: Perfil, Experiencia, Formación, Competencias, Métricas/Impacto. +- Todos los datos del CV editables sin tocar código. + +**Generador de PDF** +- Endpoint `/?cv_pdf=1` genera PDF dinámico con Chromium headless. +- Diseño fiel al documento DOCX original: columna única, navy #1B3A5C, secciones en mayúsculas con borde inferior. +- Se actualiza automáticamente al cambiar datos en el admin. +- Botón "Descargar CV PDF" en todos los temas apunta al mismo endpoint. + +**Infraestructura** +- Servidor PHP integrado en `0.0.0.0:8081` (accesible desde red local). +- URL dinámica en `wp-config.php` — funciona con cualquier IP y detrás de proxy SSL (NPM). +- Auto-updates de WordPress desactivados. +- WP Mail SMTP instalado (pendiente configuración SMTP). + +**Contenido del CV cargado** +- 5 cargos con 23 bullets completos. +- 2 títulos académicos, 10 certificaciones. +- 3 grupos de competencias (15 habilidades con porcentajes). +- Métricas, highlights, impacto, voluntariado y datos adicionales. diff --git a/CV_Patricia_Flores_Sanchez_ES_FINAL.docx b/CV_Patricia_Flores_Sanchez_ES_FINAL.docx new file mode 100644 index 0000000..544bf3d Binary files /dev/null and b/CV_Patricia_Flores_Sanchez_ES_FINAL.docx differ diff --git a/DEPLOY_WEB.md b/DEPLOY_WEB.md new file mode 100644 index 0000000..522c7d0 --- /dev/null +++ b/DEPLOY_WEB.md @@ -0,0 +1,90 @@ +# Deploy Web — pflores.linkedcorp.net + +## Previo a cualquier push + +- [ ] `git status` — sin `.env` ni `.token_gitea` en staging +- [ ] Site responde: `curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad"` → debe retornar ≥ 1 +- [ ] PDF funciona: `curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep Content-Type` → `application/pdf` +- [ ] Si hubo cambios funcionales: actualizar `VERSION` y `CHANGELOG.md` + +## Pasos — ciclo normal de cambios + +```bash +# 1. Ver qué cambió +git status +git diff --stat + +# 2. Agregar archivos (selectivo, nunca git add -A sin revisar) +git add frontend/persona-cv-theme/ +git add frontend/lumina-cv-theme/ +git add frontend/cvfolio-theme/ +git add backend/wordpress/wp-content/themes/apex-cv/ +git add backend/wordpress/wp-content/plugins/pflores-cv-fields/ +git add backend/wordpress/wp-config.php +git add README.md CHANGELOG.md VERSION PROMPT_IA.md +# ... agregar solo lo que corresponde + +# 3. Commit +git commit -m "fix: descripción del cambio" +# Prefijos: feat: / fix: / docs: / chore: / style: + +# 4. Push a Gitea +git push origin main +``` + +## Credenciales para git push + +Las credenciales de Gitea se guardan en `.token_gitea` (nunca versionado): + +```text +# Token gitea +usuario: mgonzalez +token: [tu personal access token de gitea.linkedcorp.net] + +# Gitea web +user: mgonzalez +passwd: [contraseña de acceso] +``` + +Si Git pide usuario y contraseña al hacer push, usar: +- **Usuario:** `mgonzalez` +- **Contraseña:** el personal access token (no la contraseña web) + +Crear token en Gitea: `https://gitea.linkedcorp.net/user/settings/applications` + +### Guardar credenciales para no escribirlas cada vez + +```bash +git config credential.helper store +# La primera vez que hagas push te pedirá usuario+token y los guardará +``` + +## Remoto configurado + +``` +origin https://gitea.linkedcorp.net/mgonzalez/pflores.linkedcorp.net.git +``` + +## No tocar durante el deploy + +- `.env` — credenciales de BD y servidor +- Base de datos (no hacer migrate sin confirmar) +- El proceso PHP del servidor (no reiniciar en producción sin verificar) + +## Si el servidor necesita actualizar el código + +Este proyecto corre directamente desde el directorio de trabajo. Si hay un clon separado en producción: + +```bash +# En el servidor de producción +cd /ruta/al/proyecto +git pull origin main +# Reiniciar el proceso PHP si es necesario +``` + +## Verificación post-push + +```bash +# Confirmar que Gitea recibió los cambios +# Ir a: https://gitea.linkedcorp.net/mgonzalez/pflores.linkedcorp.net +``` diff --git a/PRODUCCION.md b/PRODUCCION.md new file mode 100644 index 0000000..4ee4dbf --- /dev/null +++ b/PRODUCCION.md @@ -0,0 +1,85 @@ +# Producción — pflores.linkedcorp.net + +## Estado actual + +- Versión: `1.0.0` +- Estado: `operativo` +- Última validación: `2026-06-05` + +## Acceso público + +- URL: `https://pflores.linkedcorp.net` +- Admin: `https://pflores.linkedcorp.net/wp-admin` +- PDF: `https://pflores.linkedcorp.net/?cv_pdf=1` + +## Servidor + +- IP local: `192.168.222.40` +- Puerto WordPress: `8081` (PHP built-in server, `0.0.0.0:8081`) +- Proxy: Nginx Proxy Manager → `http://192.168.222.40:8081` +- SSL: certificado en NPM, Force SSL activado, `X-Forwarded-Proto: https` + +## Proceso WordPress + +```bash +# Ver si está corriendo +lsof -i tcp:8081 + +# Arrancar +bash /home/mgonzalez/desarrollo/web/pflores.linkedcorp.net/backend/start-wordpress.sh & + +# Detener +kill $(lsof -ti tcp:8081) +``` + +## Base de datos + +- Host: `192.168.222.20` +- Base: `paty.wordpress` +- Usuario: `paty.wordpress` +- Prefijo de tablas: `cv_` +- Credenciales completas: ver `.env` + +## Plugins activos + +| Plugin | Archivo | Función | +|--------|---------|---------| +| CMB2 | `cmb2/init.php` | Campos personalizados del CV | +| Pflores CV Fields | `pflores-cv-fields/pflores-cv-fields.php` | Admin + PDF | +| WP Mail SMTP | `wp-mail-smtp/wp_mail_smtp.php` | Email (pendiente config) | + +Verificar plugins activos: +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "SELECT option_value FROM cv_options WHERE option_name='active_plugins';" 2>/dev/null +``` + +## Tema activo + +- Slug: `persona-cv` +- Fuente: `frontend/persona-cv-theme/` (symlink) + +## Repositorio Git + +- Remoto: `https://gitea.linkedcorp.net/mgonzalez/pflores.linkedcorp.net.git` +- Rama principal: `main` +- Credenciales: `~/.gitconfig` o `.token_gitea` (no versionado) + +## Comandos de verificación rápida + +```bash +# Site responde +curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad" + +# PDF funciona +curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep Content-Type + +# Limpiar caché WP si algo no se actualiza +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "DELETE FROM cv_options WHERE option_name LIKE '_site_transient_%' OR option_name LIKE '_transient_%';" 2>/dev/null +``` + +## Pendientes + +- [ ] Configurar SMTP en WP Mail SMTP (Gmail u otro proveedor) +- [ ] Revisar si el proceso WordPress arranca automáticamente al reiniciar el servidor diff --git a/PROMPT_IA.md b/PROMPT_IA.md new file mode 100644 index 0000000..07e63bc --- /dev/null +++ b/PROMPT_IA.md @@ -0,0 +1,756 @@ +# PROMPT_IA — Guía completa del proyecto pflores.linkedcorp.net + +> Documento de referencia para asistentes de IA. Léelo completo antes de cualquier tarea. +> Última actualización: junio 2026. + +--- + +## 1. ¿Qué es este proyecto? + +Sitio web personal de **hoja de vida / currículum ejecutivo** de **Patricia Soledad Flores Sánchez**, gerente de operaciones financieras con 22+ años en banca y microfinanzas (Ecuador). + +- URL pública: `https://pflores.linkedcorp.net` +- Stack: **WordPress 6.x** + PHP 8.4 + MariaDB + **CMB2** (campos personalizados, 100 % gratuito) +- Se ejecuta con el servidor PHP incorporado (`php -S`) en el puerto **8081** +- SSL terminado por **Nginx Proxy Manager** → envía `X-Forwarded-Proto: https` +- IP local del servidor: `192.168.222.40` + +--- + +## 2. Estructura de directorios + +``` +pflores.linkedcorp.net/ +├── backend/ +│ ├── start-wordpress.sh # Arrancar el servidor PHP +│ └── wordpress/ # Instalación WordPress +│ ├── wp-config.php # Config dinámica (URL, auto-updates OFF) +│ └── wp-content/ +│ ├── plugins/ +│ │ ├── cmb2/ # CMB2 — campos personalizados (GRATIS) +│ │ └── pflores-cv-fields/ +│ │ ├── pflores-cv-fields.php # Menú admin + campos CMB2 +│ │ ├── pdf-generator.php # Generador PDF con Chromium +│ │ └── populate-data.php # Sembrador inicial (ejecutar 1 vez) +│ └── themes/ +│ ├── persona-cv/ # ACTIVO — Personage-style, rojo, timeline central +│ ├── apex-cv/ # Dark, cian, sidebar fijo izquierdo +│ ├── lumina -> ../../frontend/lumina-cv-theme # symlink +│ ├── cvolio -> ../../frontend/cvfolio-theme # symlink +│ └── ... +├── frontend/ +│ ├── persona-cv-theme/ # Fuente del tema activo (usa symlink persona-cv) +│ │ ├── style.css # Solo cabecera del tema +│ │ ├── functions.php # Setup, enqueue, helpers +│ │ ├── header.php # Header con nav y botón PDF +│ │ ├── footer.php +│ │ ├── front-page.php # PLANTILLA PRINCIPAL — toda la página CV +│ │ ├── index.php # Fallback +│ │ ├── assets/css/theme.css # Estilos completos +│ │ ├── assets/js/theme.js # JS (nav, scroll spy, animaciones) +│ │ ├── assets/images/ # patricia-flores-profile.jpeg, qr.png +│ │ ├── assets/docs/ # CV_Patricia_Flores_Sanchez_ES_FINAL.docx +│ │ └── inc/ +│ │ ├── cv-data.php # Fallback hardcodeado (función persona_cv_data()) +│ │ ├── customizer.php # Campos Customizer (mínimos) +│ │ └── visits.php # Contador de visitas +│ ├── lumina-cv-theme/ # Tema claro teal+ámbar, sidebar izquierdo +│ ├── cvfolio-theme/ # Tema oscuro glassmorphism (original) +│ └── apex-cv-theme/ # OBSOLETO — usar wp-content/themes/apex-cv +├── docs/ +│ └── instalacion-wordpress.md +├── .env # Variables de entorno (DB, puerto, etc.) +├── PROMPT_IA.md # ESTE ARCHIVO +└── README.md +``` + +> **NOTA IMPORTANTE:** El tema `apex-cv` vive directamente en +> `backend/wordpress/wp-content/themes/apex-cv/` (directorio real, no symlink) +> porque el directorio `frontend/apex-cv-theme/` tiene un problema de persistencia. +> Los demás temas activos usan symlinks desde `wp-content/themes/` hacia `frontend/`. + +--- + +## 3. Base de datos + +``` +Host: 192.168.222.20 +DB: paty.wordpress +Usuario: paty.wordpress +Password: paty.wordpress +Prefijo: cv_ → tabla principal: cv_options +``` + +Conexión de prueba: +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 +``` + +--- + +## 4. Arrancar / reiniciar el servidor + +```bash +# Ver si está corriendo +lsof -i tcp:8081 + +# Matar proceso existente +kill $(lsof -ti tcp:8081) + +# Arrancar (lee variables de .env automáticamente) +bash backend/start-wordpress.sh & +``` + +El servidor escucha en `0.0.0.0:8081` (todas las interfaces) gracias a `WP_HOST=0.0.0.0` en `.env`. + +--- + +## 5. Plugins activos y su función + +| Plugin | Propósito | Gratuito | +|--------|-----------|----------| +| `cmb2/init.php` | Campos personalizados en el admin (repeaters, options pages) | ✓ GPL | +| `pflores-cv-fields/pflores-cv-fields.php` | Páginas de opciones + función `pflores_get_cv_data()` + PDF | ✓ propio | + +**Activar / verificar plugins en la BD:** +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "SELECT option_value FROM cv_options WHERE option_name='active_plugins';" +``` + +El valor correcto debe ser: +``` +a:2:{i:0;s:13:"cmb2/init.php";i:1;s:39:"pflores-cv-fields/pflores-cv-fields.php";} +``` + +Si no, corregirlo: +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "UPDATE cv_options SET option_value='a:2:{i:0;s:13:\"cmb2/init.php\";i:1;s:39:\"pflores-cv-fields/pflores-cv-fields.php\";}' WHERE option_name='active_plugins';" +``` + +> ⚠ La serialización PHP es estricta: `s:N:` donde N es la longitud exacta en bytes. +> Siempre genera el valor con `php -r "echo serialize(['cmb2/init.php','pflores-cv-fields/pflores-cv-fields.php']);"`. + +--- + +## 6. Cambiar el tema activo + +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "UPDATE cv_options SET option_value='SLUG_TEMA' WHERE option_name IN('template','stylesheet');" +``` + +| Slug | Tema | +|------|------| +| `persona-cv` | **Activo por defecto** — Personage-style, secciones alternas, rojo | +| `lumina` | Claro, teal+ámbar, sidebar izquierdo, foto circular | +| `apex-cv` | Oscuro, cian eléctrico, sidebar fijo | +| `cvolio` | Oscuro glassmorphism, paleta café/tostado | + +--- + +## 7. Arquitectura de datos — CMB2 + +Todos los datos del CV se almacenan en `wp_options` (prefijo `cv_`) como arrays PHP serializados. Se acceden con `get_option('clave')`. + +| Clave `wp_options` | Contenido | +|--------------------|-----------| +| `pflores_perfil` | Nombre, cargo, intro, resumen, contacto, foto | +| `pflores_experiencia` | Array de cargos con bullets | +| `pflores_formacion` | Títulos académicos + certificaciones | +| `pflores_competencias` | Grupos de habilidades con porcentajes | +| `pflores_extras` | Métricas, highlights, impacto, voluntariado | + +**Función central de lectura:** +```php +pflores_get_cv_data(): array +// Retorna la misma estructura que todos los temas esperan. +// Si CMB2 no está disponible, devuelve pflores_cv_fallback_data(). +``` + +**Estructura del array retornado:** +```php +[ + 'hero' => ['name','role','role2','intro','location','email', + 'phone','linkedin','availability','focus','languages', + 'born','id','web','eyebrow'], + 'photo' => 'URL de la imagen de perfil (o vacío)', + 'summary' => ['title','body'], + 'metrics' => [['value','suffix','label'], ...], // 5 contadores + 'highlights' => [['value','suffix','label'], ...], // 4 logros grandes + 'impact' => [['title','body'], ...], // 3 áreas + 'experience' => [['period','title','company','location','lead','bullets'=>[...]], ...], + 'education' => [['year','title','institution','detail'], ...], + 'certifications' => [['year','title','issuer'], ...], + 'competencies' => ['Grupo' => [['name','pct'], ...], ...], + 'volunteer' => ['title','year','body'], + 'additional' => ['Idiomas'=>'...','Disponibilidad'=>'...','C.I.'=>'...', ...], +] +``` + +--- + +## 8. Editar datos del CV desde el admin + +URL del admin: `https://pflores.linkedcorp.net/wp-admin` +- Usuario: `pflores` +- Contraseña: en `.env` como `WP_ADMIN_PASSWORD` + +En el menú lateral aparece **"CV Patricia Flores"** con sub-páginas: +- **Perfil** → datos personales, foto, disponibilidad +- **Experiencia** → cargos (repeater, arrastrable) +- **Formación** → títulos + certificaciones +- **Competencias** → grupos de habilidades con porcentajes +- **Métricas / Impacto** → contadores, logros, áreas de impacto, voluntariado + +--- + +## 9. Agregar un nuevo campo al CV + +### 9a. Campo simple (texto, email, URL) + +**Paso 1** — Registrar el campo en `pflores-cv-fields.php` dentro de la función `pflores_register_pages()`: + +```php +// Ejemplo: añadir "Twitter/X" al perfil +$perfil->add_field([ + 'id' => 'pf_twitter', + 'name' => 'Twitter / X URL', + 'type' => 'text_url', +]); +``` + +**Paso 2** — Añadir la lectura en `pflores_get_cv_data()`: +```php +'hero' => [ + // ... campos existentes ... + 'twitter' => $perfil['pf_twitter'] ?? '', +], +``` + +**Paso 3** — Usarlo en el `front-page.php` de cada tema: +```php +$twitter = $hero['twitter'] ?? ''; +if ($twitter) : ?> + Twitter + +``` + +**Paso 4** — Si el campo debe aparecer en el PDF, añadirlo en `pdf-generator.php` dentro de `pflores_build_cv_html()`. + +--- + +### 9b. Nuevo grupo repetible (como experiencia o certificaciones) + +**Paso 1** — Registrar un nuevo CMB2 box con group en `pflores_register_pages()`: + +```php +$premios = new_cmb2_box([ + 'id' => 'pflores_premios', + 'title' => 'Premios y reconocimientos', + 'object_types' => ['options-page'], + 'option_key' => 'pflores_premios', // nombre de la opción en BD + 'parent_slug' => 'pflores_perfil', // sub-menú bajo el menú principal + 'menu_title' => 'Premios', + 'save_button' => 'Guardar cambios', + 'capability' => 'manage_options', +]); + +$gr = $premios->add_field([ + 'id' => 'pf_awards', + 'type' => 'group', + 'options' => ['group_title' => 'Premio {#}', 'add_button' => 'Añadir premio', + 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => true], +]); +$premios->add_group_field($gr, ['id' => 'year', 'name' => 'Año', 'type' => 'text']); +$premios->add_group_field($gr, ['id' => 'title', 'name' => 'Título', 'type' => 'text']); +$premios->add_group_field($gr, ['id' => 'org', 'name' => 'Organización','type' => 'text']); +``` + +**Paso 2** — Leer los datos en `pflores_get_cv_data()`: +```php +$premiosOpts = get_option('pflores_premios', []); +$awards = []; +foreach ((array) ($premiosOpts['pf_awards'] ?? []) as $item) { + $awards[] = ['year' => $item['year'] ?? '', 'title' => $item['title'] ?? '', 'org' => $item['org'] ?? '']; +} +// Agregar 'awards' => $awards al array retornado +``` + +**Paso 3** — Renderizarlo en `front-page.php` de cada tema. + +**Paso 4** — Poblar datos iniciales añadiendo al `populate-data.php`: +```php +update_option('pflores_premios', [ + 'pf_awards' => [ + ['year' => '2025', 'title' => 'Premio Excelencia', 'org' => 'Fundación FACES'], + ], +]); +``` + +--- + +## 10. Modificar datos existentes + +**Opción A** — Desde el admin de WordPress (recomendado): +- Ir a `wp-admin` → **CV Patricia Flores** → sub-página correspondiente → editar → Guardar. + +**Opción B** — Directo en BD (para migraciones o correcciones masivas): +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 -e " +UPDATE cv_options SET option_value = 'Nuevo valor' WHERE option_name = 'pflores_perfil'; +" +``` +> Para campos dentro de arrays (repeaters), es más fácil editar desde el admin o re-ejecutar `populate-data.php`. + +**Re-ejecutar el poblador de datos** (si se necesita resetear todo): +``` +https://pflores.linkedcorp.net/wp-content/plugins/pflores-cv-fields/populate-data.php?key=pflores2024 +``` +> ⚠ Esto sobreescribe TODOS los datos con los valores del archivo `populate-data.php`. + +--- + +## 11. Crear un nuevo tema + +### Convenciones obligatorias + +1. **Directorio**: Crear en `frontend/NOMBRE-cv-theme/` con subdirectorios `assets/{css,js,images,docs}` e `inc/`. +2. **Symlink**: `ln -s /ruta/absoluta/frontend/NOMBRE-cv-theme /ruta/wordpress/wp-content/themes/NOMBRE-cv` +3. **Text Domain**: usar `nombre` (minúsculas, sin guiones). +4. **Prefijo de funciones**: `nombre_` (ej: `nova_setup()`, `nova_render_nav()`). +5. **Prefijo CSS**: `nv-` (2-3 letras, sin conflictos con `pn-`, `lm-`, `ax-`, `cv-`). + +### Archivos mínimos requeridos + +``` +NOMBRE-cv-theme/ +├── style.css # Cabecera del tema (Theme Name, Text Domain, Version...) +├── functions.php # apex_setup, enqueue, nav helpers +├── header.php #
+ nav + botón PDF +├── footer.php # +├── front-page.php # TODA la lógica y HTML del CV +├── index.php # Fallback vacío o mínimo +└── inc/ + ├── cv-data.php # Función NOMBRE_cv_data() con fallback hardcodeado + ├── customizer.php # Opcional, puede estar vacío + └── visits.php # Contador de visitas (copiar de otro tema y renombrar funciones) +``` + +### Estructura obligatoria de `functions.php` + +```php + $items) : ?> + + + + + + + +``` + +### Botón de descarga PDF (obligatorio en todos los temas) + +```php + + + Descargar CV PDF + +``` + +### `inc/cv-data.php` — función de fallback + +Debe contener **todos los datos reales** de Patricia Flores, idénticos a los que están en la BD. Copiar la función de otro tema y renombrar a `NOMBRE_cv_data()`. + +> ⚠ Las competencias del fallback deben tener formato array: +> `['name' => 'Gestión operativa', 'pct' => 95]` + +### Activar el nuevo tema + +```bash +# 1. Crear el symlink +ln -s /ruta/absoluta/frontend/NOMBRE-cv-theme \ + /ruta/absoluta/backend/wordpress/wp-content/themes/NOMBRE-cv + +# 2. Activar en la BD +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "UPDATE cv_options SET option_value='NOMBRE-cv' WHERE option_name IN('template','stylesheet');" +``` + +> ⚠ **Evitar el slug `apex`** (existe un tema comercial con ese nombre en WordPress.org). +> Usar siempre slugs compuestos: `nova-cv`, `slate-cv`, `forma-cv`, etc. + +--- + +## 12. Crear / modificar menús de navegación + +Los menús de navegación son listas de anclas (`#seccion`) dentro del mismo tema. No se usan menús de WordPress por defecto. + +### Modificar los ítems del menú en un tema + +En `functions.php` de cada tema, hay una función `NOMBRE_render_nav()`: + +```php +function persona_render_nav(): string { + $items = [ + '#sobre-mi' => 'Sobre mí', + '#impacto' => 'Impacto', + '#experiencia' => 'Experiencia', + '#formacion' => 'Formación', + '#competencias' => 'Competencias', + '#certificaciones' => 'Certificaciones', + '#contacto' => 'Contacto', + ]; + // Para añadir un ítem: '#premios' => 'Premios', + // Para eliminar: quitar la línea correspondiente + // Para reordenar: cambiar el orden del array + ... +} +``` + +El `href` debe coincidir con el `id` de la sección en `front-page.php`: +```html +Premios y reconocimientos
' . $awardsHtml . +``` + +--- + +## 14. Archivo `.env` + +``` +WP_ENV=local +WP_HOST=0.0.0.0 +WP_PORT=8081 +WP_URL=http://192.168.222.40:8081 + +DB_NAME=paty.wordpress +DB_USER=paty.wordpress +DB_PASSWORD=paty.wordpress +DB_HOST=192.168.222.20 +DB_PREFIX=cv_ + +WP_ADMIN_USER=pflores +WP_ADMIN_PASSWORD=HXk4boxuI1QYum3jKL +``` + +--- + +## 15. Configuración `wp-config.php` clave + +```php +// Auto-updates DESACTIVADAS (protege archivos de temas y plugins locales) +define('AUTOMATIC_UPDATER_DISABLED', true); +define('WP_AUTO_UPDATE_CORE', false); + +// URL dinámica — funciona con cualquier IP o dominio, y detrás de proxy SSL (NPM) +$_wpIsHttps = /* detecta X-Forwarded-Proto: https */; +define('WP_HOME', $_wpProto . '://' . $_wpHost); +define('WP_SITEURL', $_wpProto . '://' . $_wpHost); +``` + +--- + +## 16. Convenciones de código + +| Elemento | Convención | +|----------|-----------| +| PHP | `declare(strict_types=1)` en todos los archivos | +| Funciones WordPress | Usar `esc_html()`, `esc_url()`, `esc_attr()` en toda salida HTML | +| Prefijos | Funciones y opciones siempre con prefijo del plugin/tema para evitar conflictos | +| Comentarios | Solo cuando el "por qué" no es obvio. Sin comentarios de "qué hace" | +| Serialización PHP en SQL | Generar con `php -r "echo serialize([...]);"` — nunca manual | +| Auto-updates WP | **Siempre desactivadas** — no tocar `wp-config.php` en ese bloque | + +--- + +## 17. Tareas comunes — referencia rápida + +### Verificar que todo funciona +```bash +curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad" +# Debe retornar >= 1 +``` + +### Ver el tema activo +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "SELECT option_value FROM cv_options WHERE option_name='template';" 2>/dev/null +``` + +### Ver todos los datos del CV en BD +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "SELECT option_name, LENGTH(option_value) as bytes FROM cv_options WHERE option_name LIKE 'pflores_%' ORDER BY option_name;" 2>/dev/null +``` + +### Limpiar caché de WordPress (transients) +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "DELETE FROM cv_options WHERE option_name LIKE '_site_transient_%' OR option_name LIKE '_transient_%';" 2>/dev/null +``` + +### Verificar los 4 temas en un loop +```bash +for slug in persona-cv lumina apex-cv cvolio; do + mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "UPDATE cv_options SET option_value='$slug' WHERE option_name IN('template','stylesheet');" 2>/dev/null + sleep 0.4 + count=$(curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad" || true) + echo "[$slug] Patricia aparece: $count veces" +done +# Restaurar: +mysql ... -e "UPDATE cv_options SET option_value='persona-cv' WHERE option_name IN('template','stylesheet');" +``` + +### Probar la descarga del PDF +```bash +curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep -E "Content-Type|Content-Disposition" +# Debe mostrar: application/pdf y attachment; filename="CV_Patricia_Flores_Sanchez.pdf" +``` + +### Comprobar PHP syntax de un tema +```bash +php -l /ruta/al/tema/front-page.php +php -l /ruta/al/tema/functions.php +``` + +--- + +## 18. Errores frecuentes y soluciones + +| Error | Causa probable | Solución | +|-------|---------------|----------| +| Página en blanco o "error crítico" | PHP fatal en el tema | Ver `debug.log`, buscar la línea del error | +| CSS sin cargar / página sin estilos | Mixed content (HTTP vs HTTPS) | Verificar `wp-config.php` — bloque de proxy SSL | +| CMB2 / plugin no carga | Serialización incorrecta en `active_plugins` | Regenerar con `php -r "echo serialize([...]);"` | +| `pflores_get_cv_data()` retorna vacío | Datos no guardados en BD | Re-ejecutar `populate-data.php?key=pflores2024` + limpiar caché | +| Apex-cv vacío o sin archivos | Tema apex-cv depende de directorio real, no symlink | Los archivos van en `wp-content/themes/apex-cv/`, no en `frontend/` | +| PDF vacío o error 500 | Chromium no encontrado o falla | `which chromium chromium-browser google-chrome` | +| `update_option` no guarda | Caché de WordPress | Limpiar transients (ver sección 17) | + +--- + +## 19. Acerca de los temas — resumen visual + +| Tema | Estilo | Layout | Fuente | Acento | +|------|--------|--------|--------|--------| +| `persona-cv` | Personage/Pixflow, secciones alternas | Full-width, nav sticky top | Raleway + Open Sans | Rojo `#c0392b` | +| `lumina` | Claro, moderno, alegre | Sidebar IZQUIERDO fijo | Plus Jakarta Sans | Teal `#0d9488` + Ámbar | +| `apex-cv` | Oscuro, serio, editorial | Sidebar IZQUIERDO fijo | Space Grotesk | Cian `#22d3ee` + Dorado | +| `cvolio` | Oscuro glassmorphism | Hero dividido + sidebar DERECHO | Manrope + Cormorant | Café/tostado | + +--- + +## 20. Notas importantes para la IA asistente + +1. **No usar ACF** (Advanced Custom Fields). Tiene Repeater solo en versión de pago. Usar CMB2. +2. **No instalar plugins de pago**. Todo debe ser 100 % gratuito. +3. **No tocar el bloque de auto-updates** en `wp-config.php`. +4. **Verificar serialización PHP** antes de cualquier `UPDATE` en `active_plugins` o similar. +5. **Limpiar caché** después de modificar datos en BD directamente. +6. **La foto de perfil** es `assets/images/patricia-flores-profile.jpeg` en cada tema — NO usar el placeholder SVG como defecto. +7. **El CV en docx** (`CV_Patricia_Flores_Sanchez_ES_FINAL.docx`) es solo para descarga — el contenido de la web se gestiona por CMB2. +8. **El PDF** se genera dinámicamente con Chromium desde `/?cv_pdf=1` — no hay PDF estático. +9. **Los 4 temas leen de CMB2** mediante `pflores_get_cv_data()`. Si esa función no existe (plugin inactivo), cada tema usa su propio fallback hardcodeado. +10. **Siempre verificar** con `curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad"` que el sitio responde correctamente después de cualquier cambio. + +--- + +## 21. Git y Gitea — versionar y subir + +### Repositorio + +- **Remoto:** `https://gitea.linkedcorp.net/mgonzalez/pflores.linkedcorp.net.git` +- **Rama:** `main` +- **Credenciales:** archivo local `.token_gitea` (nunca versionado) + +### Flujo estándar de commit + push + +```bash +# 1. Revisar qué cambió +git status +git diff --stat + +# 2. Agregar archivos — SELECTIVO (nunca git add -A sin revisar) +git add frontend/ # temas propios +git add backend/wordpress/wp-content/plugins/pflores-cv-fields/ +git add backend/wordpress/wp-content/themes/apex-cv/ +git add backend/wordpress/wp-config.php +git add CHANGELOG.md VERSION README.md PROMPT_IA.md + +# 3. Confirmar que .env y .token_gitea NO están en staging +git status | grep -E "\.env|\.token" # debe retornar vacío + +# 4. Commit +git commit -m "tipo: descripción corta" +# Prefijos: feat: / fix: / docs: / chore: / style: + +# 5. Push +git push origin main +``` + +### Credenciales para push + +Al hacer `git push` por primera vez pedirá usuario y contraseña: +- **Usuario:** `mgonzalez` +- **Contraseña:** Personal Access Token (NO la contraseña web de Gitea) + +Crear token: `https://gitea.linkedcorp.net/user/settings/applications` + +Para no escribir credenciales cada vez: +```bash +git config credential.helper store +# La primera vez las guarda; las siguientes son automáticas +``` + +Guardar también en `.token_gitea` (local, nunca versionar): +```text +# Token gitea +usuario: mgonzalez +token: [token de gitea.linkedcorp.net] +``` + +### Qué SE versiona vs qué NO + +**SE versiona:** +- `frontend/` (todos los temas propios) +- `backend/wordpress/wp-content/plugins/pflores-cv-fields/` +- `backend/wordpress/wp-content/themes/apex-cv/` +- `backend/wordpress/wp-config.php` +- `backend/start-wordpress.sh`, `docs/`, `*.md`, `VERSION` +- `CV_Patricia_Flores_Sanchez_ES_FINAL.docx`, imágenes del CV + +**NO se versiona:** +- `.env`, `.token_gitea`, `LOCAL_DEPLOY_CONTEXT.md` +- WordPress core (`wp-admin/`, `wp-includes/`) +- Plugins de terceros (`cmb2/`, `wp-mail-smtp/`) +- Temas por defecto de WP (`twenty*/`) +- `wp-content/debug.log`, `uploads/`, `languages/` + +### Versionar antes de hacer push + +Ver `VERSIONADO.md` para decidir si subir `X`, `Y` o `Z`: +```bash +# Actualizar VERSION +echo "1.0.1" > VERSION + +# Anotar cambio en CHANGELOG.md +# Luego commit y push normal +``` diff --git a/README.md b/README.md index 38f43f1..6227325 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,89 @@ -# CVolio WP +# pflores.linkedcorp.net -Tema WordPress moderno para hoja de vida, preparado para pruebas locales en este directorio. +Sitio web de currículum ejecutivo de **Patricia Soledad Flores Sánchez** — Gerente / Jefa de Operaciones Financieras, sector financiero y microfinanzas. + +## Stack + +- **CMS:** WordPress 6.x (PHP 8.4, servidor PHP integrado) +- **Base de datos:** MariaDB en `192.168.222.20` +- **Campos del CV:** CMB2 (100 % gratuito, GPL) +- **PDF:** Chromium headless (`/?cv_pdf=1`) +- **Email:** WP Mail SMTP (configurable) +- **Deploy:** Debian 12 · Nginx Proxy Manager → `192.168.222.40:8081` +- **URL pública:** `https://pflores.linkedcorp.net` ## Estructura -- `frontend/cvfolio-theme`: tema editable. -- `backend/wordpress`: instalación local de WordPress. -- `docs/`: notas de instalación y uso. - -## Estado actual - -- WordPress descargado en local. -- Tema base responsive creado. -- Puerto sugerido preparado: `8081`. -- Pendiente para ejecución completa: extensiones PHP de WordPress y una base MariaDB/MySQL disponible. +``` +pflores.linkedcorp.net/ +├── frontend/ +│ ├── persona-cv-theme/ ← ACTIVO — Personage-style, rojo, timeline central +│ ├── lumina-cv-theme/ ← Claro, teal + ámbar, sidebar izquierdo +│ └── cvfolio-theme/ ← Oscuro, glassmorphism original +├── backend/ +│ ├── start-wordpress.sh +│ └── wordpress/ +│ ├── wp-config.php +│ └── wp-content/ +│ ├── plugins/pflores-cv-fields/ ← plugin propio: CMB2 + PDF +│ └── themes/apex-cv/ ← tema oscuro cian +├── docs/ +├── .env ← local, no versionado +├── .env.example +├── VERSION +├── CHANGELOG.md +├── PROMPT_IA.md ← guía completa para IA +└── README.md +``` ## Arranque rápido -1. Copia `.env.example` a `.env`. -2. Ajusta credenciales de base de datos. -3. Revisa `docs/instalacion-wordpress.md`. -4. Ejecuta `backend/start-wordpress.sh`. +```bash +# 1. Copiar variables de entorno +cp .env.example .env +# 2. Editar .env con credenciales reales +# 3. Arrancar el servidor WordPress +bash backend/start-wordpress.sh & +# 4. Abrir http://127.0.0.1:8081 +``` -## Git +El servidor escucha en `0.0.0.0:8081` (accesible desde la red local). -El repositorio fue inicializado localmente. Falta enlazar remoto si luego quieres subirlo a Gitea. +## Temas disponibles + +| Slug | Descripción | Estado | +|------|-------------|--------| +| `persona-cv` | Personage-style, rojo, secciones alternas, Raleway | **Activo** | +| `lumina` | Claro, teal+ámbar, sidebar izq., Plus Jakarta Sans | Disponible | +| `apex-cv` | Oscuro, cian eléctrico, sidebar fijo, Space Grotesk | Disponible | +| `cvolio` | Oscuro glassmorphism, café/tostado, Manrope | Disponible | + +Cambiar tema activo: +```bash +mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \ + -e "UPDATE cv_options SET option_value='SLUG' WHERE option_name IN('template','stylesheet');" +``` + +## Editar el CV + +Acceder al admin: `https://pflores.linkedcorp.net/wp-admin` +- Usuario: `pflores` +- Menú: **CV Patricia Flores** → sub-páginas por sección + +Los datos se almacenan en CMB2 (opciones de WordPress). Cada cambio se refleja automáticamente en la web y en el PDF descargable. + +## Descargar CV en PDF + +El PDF se genera dinámicamente con Chromium headless: +``` +GET /?cv_pdf=1 → descarga CV_Patricia_Flores_Sanchez.pdf +``` + +## Documentación relacionada + +- `PROMPT_IA.md` — guía completa para asistentes de IA y desarrolladores +- `DEPLOY_WEB.md` — pasos para versionar y desplegar +- `PRODUCCION.md` — estado del servidor de producción +- `VERSIONADO.md` — reglas de versiones +- `CHANGELOG.md` — historial de cambios +- `docs/instalacion-wordpress.md` — configuración local diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/VERSIONADO.md b/VERSIONADO.md new file mode 100644 index 0000000..8f04d08 --- /dev/null +++ b/VERSIONADO.md @@ -0,0 +1,50 @@ +# Versionado — pflores.linkedcorp.net + +## Esquema: `X.Y.Z` + +| Nivel | Cuándo subirlo | +|-------|---------------| +| `X` | Rediseño completo, cambio de CMS, rotura de compatibilidad mayor | +| `Y` | Nuevo tema completo, nueva sección del CV, nuevo módulo funcional | +| `Z` | Correcciones visuales, ajuste de texto, nuevo campo, fix de bug | + +## Qué se versiona + +- `frontend/` — todos los temas WordPress propios +- `backend/wordpress/wp-content/plugins/pflores-cv-fields/` — plugin propio +- `backend/wordpress/wp-content/themes/apex-cv/` — tema en directorio real +- `backend/wordpress/wp-config.php` — sin credenciales (usa variables de entorno) +- `backend/start-wordpress.sh` +- `docs/` +- `README.md`, `PROMPT_IA.md`, `CHANGELOG.md`, `VERSION`, `VERSIONADO.md` +- `PRODUCCION.md`, `DEPLOY_WEB.md` +- `.env.example` +- `.gitignore` +- Archivos de assets del CV: `CV_Patricia_Flores_Sanchez_ES_FINAL.docx`, imágenes QR/perfil + +## Qué NO se versiona + +- `.env` — credenciales locales +- `.token_gitea` — token de acceso Gitea +- `backend/wordpress/wp-admin/` y `wp-includes/` — núcleo de WordPress +- `backend/wordpress/wp-content/plugins/cmb2/` — plugin de terceros descargado +- `backend/wordpress/wp-content/plugins/wp-mail-smtp/` — plugin de terceros +- `backend/wordpress/wp-content/themes/twenty*/` — temas por defecto +- `backend/wordpress/wp-content/uploads/` — media subida +- `backend/wordpress/wp-content/debug.log` +- `.DS_Store`, `.claude/` +- `LOCAL_DEPLOY_CONTEXT.md` — contexto local con credenciales + +## Checklist antes de cada commit + +1. `git status` — revisar qué se incluye +2. Verificar que `.env` y `.token_gitea` NO están en staging +3. Si hay cambios funcionales → actualizar `VERSION` y `CHANGELOG.md` +4. Mensaje de commit claro: `feat:`, `fix:`, `chore:`, `docs:` + +## Checklist antes de subir a Gitea (`git push`) + +1. Todo el checklist de commit cumplido +2. Site funcionando: `curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad"` +3. PDF funcionando: `curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep Content-Type` +4. Ver `DEPLOY_WEB.md` para pasos completos diff --git a/backend/start-wordpress.sh b/backend/start-wordpress.sh index 513972d..7b13f99 100755 --- a/backend/start-wordpress.sh +++ b/backend/start-wordpress.sh @@ -17,9 +17,10 @@ WP_HOST="${WP_HOST:-127.0.0.1}" required_modules=("mysqli" "dom") missing_modules=() +php_modules="$(php -m | tr '[:upper:]' '[:lower:]')" for module in "${required_modules[@]}"; do - if ! php -m | grep -qi "^${module}$"; then + if ! printf '%s\n' "$php_modules" | grep -qx "$module"; then missing_modules+=("$module") fi done diff --git a/backend/wordpress/wp-config.php b/backend/wordpress/wp-config.php index 6d10f0e..088b4ea 100644 --- a/backend/wordpress/wp-config.php +++ b/backend/wordpress/wp-config.php @@ -25,11 +25,34 @@ define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); -if (getenv('WP_URL')) { - define('WP_HOME', getenv('WP_URL')); - define('WP_SITEURL', getenv('WP_URL')); +// Desactivar actualizaciones automáticas para proteger archivos de temas y plugins locales +define('AUTOMATIC_UPDATER_DISABLED', true); +define('WP_AUTO_UPDATE_CORE', false); + +// URL dinámica con soporte de reverse proxy (NPM, Nginx, Traefik…). +// NPM termina SSL y reenvía como HTTP, pero manda X-Forwarded-Proto: https. +$_wpIsHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') + || (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on'); + +// X-Forwarded-Host contiene el dominio público cuando hay proxy. +// Fallback a HTTP_HOST (que NPM también pasa correctamente). +$_wpHost = $_SERVER['HTTP_X_FORWARDED_HOST'] + ?? $_SERVER['HTTP_HOST'] + ?? '127.0.0.1:8081'; + +$_wpProto = $_wpIsHttps ? 'https' : 'http'; +define('WP_HOME', $_wpProto . '://' . $_wpHost); +define('WP_SITEURL', $_wpProto . '://' . $_wpHost); + +// Necesario para que WordPress confíe en el header del proxy +// y no entre en bucle de redirección HTTP→HTTPS. +if ($_wpIsHttps) { + $_SERVER['HTTPS'] = 'on'; } +unset($_wpIsHttps, $_wpHost, $_wpProto); + if (! defined('ABSPATH')) { define('ABSPATH', __DIR__ . '/'); } diff --git a/backend/wordpress/wp-content/plugins/pflores-cv-fields/pdf-generator.php b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pdf-generator.php new file mode 100644 index 0000000..07450ca --- /dev/null +++ b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pdf-generator.php @@ -0,0 +1,499 @@ + 500]); + } + + $cv = function_exists('pflores_get_cv_data') ? pflores_get_cv_data() : []; + $html = pflores_build_cv_html($cv); + $ts = time(); + $tmpDir = sys_get_temp_dir(); + $htmlFile = $tmpDir . '/cv_pf_' . $ts . '.html'; + $pdfFile = $tmpDir . '/cv_pf_' . $ts . '.pdf'; + + file_put_contents($htmlFile, $html); + + $cmd = escapeshellcmd($chromium) + . ' --headless --no-sandbox --disable-gpu' + . ' --run-all-compositor-stages-before-draw' + . ' --print-to-pdf=' . escapeshellarg($pdfFile) + . ' --print-to-pdf-no-header' + . ' "file://' . $htmlFile . '"' + . ' 2>/dev/null'; + + exec($cmd); + @unlink($htmlFile); + + if (! file_exists($pdfFile) || filesize($pdfFile) < 1024) { + @unlink($pdfFile); + wp_die('No se pudo generar el PDF.', 'Error PDF', ['response' => 500]); + } + + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="CV_Patricia_Flores_Sanchez.pdf"'); + header('Content-Length: ' . filesize($pdfFile)); + header('Cache-Control: no-cache, no-store, must-revalidate'); + readfile($pdfFile); + @unlink($pdfFile); + exit; +} + +function pflores_find_chromium(): string +{ + foreach (['chromium', 'chromium-browser', 'google-chrome', 'google-chrome-stable'] as $bin) { + $path = trim((string) shell_exec('which ' . escapeshellarg($bin) . ' 2>/dev/null')); + if ($path !== '' && file_exists($path)) { + return $path; + } + } + return ''; +} + +/* ═══════════════════════════════════════════════════════════ + CONSTRUCTOR HTML — réplica fiel del DOCX + ═══════════════════════════════════════════════════════════ */ +function pflores_build_cv_html(array $cv): string +{ + /* ── datos ── */ + $hero = $cv['hero'] ?? []; + $metrics = $cv['metrics'] ?? []; + $highlights = $cv['highlights'] ?? []; + $summary = $cv['summary'] ?? []; + $impact = $cv['impact'] ?? []; + $experience = $cv['experience'] ?? []; + $education = $cv['education'] ?? []; + $certifications = $cv['certifications'] ?? []; + $competencies = $cv['competencies'] ?? []; + $volunteer = $cv['volunteer'] ?? []; + $additional = $cv['additional'] ?? []; + + $name = $hero['name'] ?? ''; + $role = $hero['role'] ?? ''; + $role2 = $hero['role2'] ?? ''; + $location = $hero['location'] ?? ''; + $email = $hero['email'] ?? ''; + $phone = $hero['phone'] ?? ''; + $linkedin = $hero['linkedin'] ?? ''; + $avail = $hero['availability'] ?? ''; + $focus = $hero['focus'] ?? ''; + $langs = $hero['languages'] ?? ''; + $web = $hero['web'] ?? ''; + $intro = $hero['intro'] ?? ''; + + /* ── helpers ── */ + $h = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $nl = "\n"; + + /* ── estilos (replica el DOCX) ── */ + $css = ' +* { box-sizing: border-box; margin: 0; padding: 0; } + +@page { + size: A4; + margin: 16mm 18mm 16mm 18mm; +} + +body { + font-family: Arial, Helvetica, sans-serif; + font-size: 9pt; + color: #3A3A3A; + background: #ffffff; + line-height: 1.45; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +/* ── CABECERA ── */ +.cv-name { + font-size: 19pt; + font-weight: 700; + color: #1B3A5C; + line-height: 1.1; + margin-bottom: 3px; +} +.cv-role { + font-size: 10pt; + font-weight: 700; + color: #1B3A5C; + margin-bottom: 2px; +} +.cv-contact { + font-size: 8pt; + color: #666666; + margin-bottom: 10px; +} +.cv-contact span { margin: 0 4px; } + +/* ── SECCIONES ── */ +.sec { + margin-bottom: 10px; +} +.sec-title { + font-size: 10pt; + font-weight: 700; + color: #1B3A5C; + text-transform: uppercase; + letter-spacing: .04em; + border-bottom: 1.5px solid #1B3A5C; + padding-bottom: 2px; + margin-bottom: 6px; +} + +/* ── PERFIL ── */ +.profile-text { + font-size: 9pt; + color: #3A3A3A; + line-height: 1.55; + text-align: justify; +} + +/* ── EXPERIENCIA ── */ +.exp-item { margin-bottom: 8px; } +.exp-header { + display: flex; + justify-content: space-between; + align-items: baseline; +} +.exp-title { + font-size: 10pt; + font-weight: 700; + color: #1B3A5C; +} +.exp-period { + font-size: 9pt; + color: #3A3A3A; + font-style: italic; + white-space: nowrap; + margin-left: 8px; +} +.exp-company { + font-size: 9pt; + color: #666666; + margin-bottom: 3px; +} +.exp-desc { + font-size: 9pt; + color: #3A3A3A; + margin-bottom: 3px; + font-style: italic; +} +.exp-bullets { + margin: 0; + padding-left: 0; + list-style: none; +} +.exp-bullets li { + font-size: 9pt; + color: #3A3A3A; + padding-left: 12px; + position: relative; + margin-bottom: 2px; + line-height: 1.45; + text-align: justify; +} +.exp-bullets li::before { + content: "•"; + position: absolute; + left: 0; + color: #1B3A5C; + font-weight: 700; +} + +/* ── FORMACIÓN ── */ +.edu-item { + margin-bottom: 6px; + display: flex; + justify-content: space-between; + align-items: flex-start; +} +.edu-left { flex: 1; } +.edu-degree { + font-size: 9pt; + font-weight: 700; + color: #1B3A5C; +} +.edu-institution { + font-size: 8pt; + color: #666666; + margin-top: 1px; +} +.edu-detail { + font-size: 8pt; + color: #666666; + font-style: italic; + margin-top: 1px; +} +.edu-year { + font-size: 9pt; + font-weight: 700; + color: #1B3A5C; + white-space: nowrap; + margin-left: 8px; +} + +/* ── CERTIFICACIONES ── */ +.cert-item { + display: flex; + justify-content: space-between; + font-size: 8pt; + color: #3A3A3A; + margin-bottom: 2.5px; + line-height: 1.4; +} +.cert-title { flex: 1; padding-right: 8px; } +.cert-year { color: #1B3A5C; font-weight: 700; white-space: nowrap; } + +/* ── COMPETENCIAS ── */ +.comp-line { + font-size: 9pt; + color: #3A3A3A; + margin-bottom: 3px; + line-height: 1.45; +} +.comp-line strong { color: #1B3A5C; } + +/* ── VOLUNTARIADO / ADICIONAL ── */ +.vol-header { + display: flex; + justify-content: space-between; + align-items: baseline; +} +.vol-title { font-size: 9pt; font-weight: 700; color: #1B3A5C; } +.vol-year { font-size: 9pt; font-weight: 700; color: #1B3A5C; } +.vol-body { font-size: 9pt; color: #3A3A3A; line-height: 1.45; margin-top: 1px; } + +.add-line { + font-size: 9pt; + color: #3A3A3A; + margin-bottom: 2px; +} +.add-line strong { color: #1B3A5C; } + +.cv-footer-line { + font-size: 8pt; + color: #999999; + text-align: right; + margin-top: 8px; + border-top: .5px solid #dddddd; + padding-top: 4px; +} +'; + + /* ═══════════ SECCIÓN: PERFIL PROFESIONAL ═══════════ */ + + // Combina intro + summary (como el DOCX) + $summaryBody = $summary['body'] ?? ''; + $profileFull = trim($intro); + if ($summaryBody && $summaryBody !== $intro) { + // Evitar duplicar si intro ya contiene el resumen + if (strpos($profileFull, substr($summaryBody, 0, 30)) === false) { + $profileFull .= ' ' . $summaryBody; + } + } + + /* ═══════════ SECCIÓN: EXPERIENCIA ═══════════ */ + $expHtml = ''; + foreach ($experience as $job) { + $expHtml .= '' . $company . ($loc ? ' | ' . $loc : '') . '
'; + } + + if (! empty($job['lead'])) { + $expHtml .= '' . $h($job['lead']) . '
'; + } + + if (! empty($job['bullets'])) { + $expHtml .= '' . $h($edu['title'] ?? '') . '
'; + $eduHtml .= '' . $h($edu['institution'] ?? '') . '
'; + if (! empty($edu['detail'])) { + $eduHtml .= '' . $h($edu['detail']) . '
'; + } + $eduHtml .= '' . $h($groupName) . ': ' . $h($skillsStr) . '
'; + } + + /* ═══════════ SECCIÓN: VOLUNTARIADO ═══════════ */ + $volHtml = ''; + if (! empty($volunteer['title'])) { + $volHtml .= '' . $h($volunteer['body']) . '
'; + } + } + + /* ═══════════ SECCIÓN: INFORMACIÓN ADICIONAL ═══════════ */ + $addHtml = ''; + $addLines = [ + 'Idiomas' => $langs, + 'Disponibilidad' => $avail, + 'Enfoque' => $focus, + ]; + // Añadir los del array additional filtrando redundancias + foreach ($additional as $k => $v) { + if ($v !== '' && ! in_array($k, ['Disponibilidad', 'Idiomas'], true)) { + $addLines[$k] = $v; + } + } + foreach ($addLines as $k => $v) { + if ($v === '') continue; + $addHtml .= '' . $h($k) . ': ' . $h($v) . '
'; + } + if ($web) { + $addHtml .= 'Web: ' . $h($web) . '
'; + } + + /* ═══════════ LÍNEA DE CONTACTO (cabecera) ═══════════ */ + $contactParts = []; + if ($email) $contactParts[] = 'Correo: ' . $email; + if ($phone) $contactParts[] = 'Tel.: ' . $phone; + if ($linkedin) { + // Mostrar URL limpia sin https://www. + $lnDisplay = preg_replace('#^https?://(www\.)?#', '', urldecode($linkedin)); + $contactParts[] = 'LinkedIn: ' . $lnDisplay; + } + if ($location) $contactParts[] = $location; + $contactLine = implode(' | ', $contactParts); + + /* ═══════════ TÍTULO del documento ═══════════ */ + $fullRole = $role . ($role2 ? ' | ' . $role2 : ''); + $year = gmdate('Y'); + + /* ═══════════ HTML COMPLETO ═══════════ */ + return ' + + + +' . $h($name) . '
+' . $h($fullRole) . '
+' . $h($contactLine) . '
+ + +Perfil profesional
+' . $h($profileFull) . '
+Experiencia profesional
+ ' . $expHtml . ' +Formación académica
+ ' . $eduHtml . ' +Certificaciones relevantes
+ ' . $certHtml . ' +Competencias clave
+ ' . $compHtml . ' +Voluntariado
+ ' . $volHtml . ' +Información adicional
+ ' . $addHtml . ' +⚠ Elimina este archivo una vez verificado.
+ diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/css/theme.css b/backend/wordpress/wp-content/themes/apex-cv/assets/css/theme.css new file mode 100644 index 0000000..0390eb9 --- /dev/null +++ b/backend/wordpress/wp-content/themes/apex-cv/assets/css/theme.css @@ -0,0 +1,1194 @@ +/* ========================================================================== + Apex CV — theme.css + Dark professional CV theme · Space Grotesk + ========================================================================== */ + +/* -------------------------------------------------------------------------- + 0. CSS custom properties + -------------------------------------------------------------------------- */ +:root { + --ax-bg: #0b0d12; + --ax-surface: #11141c; + --ax-surface-2: #161a26; + --ax-surface-3: #1c2133; + --ax-border: #1e2235; + --ax-border-light: #252b40; + --ax-text: #e0e3ef; + --ax-muted: #7a8099; + --ax-subtle: #4a5268; + --ax-cyan: #22d3ee; + --ax-cyan-dim: #0e7490; + --ax-cyan-glow: rgba(34, 211, 238, .15); + --ax-gold: #f0b429; + --ax-gold-dim: rgba(240, 180, 41, .12); + --ax-green: #22c55e; + --ax-green-dim: rgba(34, 197, 94, .15); + --ax-red: #f87171; + + --sidebar-w: 280px; + --sidebar-w-sm: 240px; + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + --shadow-card: 0 1px 3px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.4); + --shadow-raised: 0 4px 16px rgba(0,0,0,.6); + --transition: 150ms cubic-bezier(.4,0,.2,1); +} + +/* -------------------------------------------------------------------------- + 1. Reset & base + -------------------------------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + font-size: 16px; + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; +} + +body { + font-family: 'Space Grotesk', system-ui, sans-serif; + background: var(--ax-bg); + color: var(--ax-text); + line-height: 1.65; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.25; + color: var(--ax-text); +} + +a { + color: var(--ax-cyan); + text-decoration: none; + transition: color var(--transition), opacity var(--transition); +} +a:hover { opacity: .8; } + +img { display: block; max-width: 100%; height: auto; } + +ul, ol { list-style: none; } + +/* -------------------------------------------------------------------------- + 2. Layout — shell + -------------------------------------------------------------------------- */ +body.apex-shell { overflow-x: hidden; } + +.ax-layout { + display: flex; + min-height: 100vh; +} + +/* -------------------------------------------------------------------------- + 3. Sidebar + -------------------------------------------------------------------------- */ +.ax-sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: var(--sidebar-w); + background: var(--ax-surface); + border-right: 1px solid var(--ax-border); + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + gap: 0; + z-index: 100; + scrollbar-width: thin; + scrollbar-color: var(--ax-border) transparent; + transition: transform var(--transition), box-shadow var(--transition); +} +.ax-sidebar::-webkit-scrollbar { width: 4px; } +.ax-sidebar::-webkit-scrollbar-track { background: transparent; } +.ax-sidebar::-webkit-scrollbar-thumb { background: var(--ax-border); border-radius: var(--radius-full); } + +/* subtle radial glow in the top-left corner */ +.ax-sidebar::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 260px; + background: radial-gradient(ellipse 140px 120px at 50% -10%, rgba(34,211,238,.08) 0%, transparent 70%); + pointer-events: none; + z-index: 0; +} + +/* -------------------------------------------------------------------------- + 4. Profile block + -------------------------------------------------------------------------- */ +.ax-profile { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 2rem 1.25rem 1.5rem; + gap: .5rem; +} + +.ax-profile__avatar-wrap { + position: relative; + margin-bottom: .5rem; +} + +.ax-profile__avatar { + width: 108px; + height: 108px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--ax-border-light); + /* pulsing cyan ring */ + box-shadow: + 0 0 0 3px var(--ax-surface), + 0 0 0 5px var(--ax-cyan-dim), + 0 0 0 6px var(--ax-surface); + animation: ax-avatar-ring 3s ease-in-out infinite; +} + +@keyframes ax-avatar-ring { + 0%, 100% { box-shadow: 0 0 0 3px var(--ax-surface), 0 0 0 5px var(--ax-cyan-dim), 0 0 0 6px var(--ax-surface); } + 50% { box-shadow: 0 0 0 3px var(--ax-surface), 0 0 0 6px rgba(34,211,238,.35), 0 0 0 8px var(--ax-surface); } +} + +/* green availability dot */ +.ax-profile__status { + position: absolute; + bottom: 4px; + right: 4px; + width: 13px; + height: 13px; + background: var(--ax-green); + border-radius: 50%; + border: 2px solid var(--ax-surface); + animation: ax-blink 2.5s ease-in-out infinite; +} + +@keyframes ax-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: .4; } +} + +.ax-profile__name { + font-size: 1rem; + font-weight: 700; + letter-spacing: -.01em; + color: var(--ax-text); + line-height: 1.3; +} + +.ax-profile__role { + font-size: .75rem; + font-weight: 600; + color: var(--ax-cyan); + letter-spacing: .02em; + text-transform: uppercase; + line-height: 1.4; +} + +.ax-profile__role2 { + font-size: .7rem; + color: var(--ax-muted); + line-height: 1.4; +} + +/* -------------------------------------------------------------------------- + 5. Sidebar navigation + -------------------------------------------------------------------------- */ +.ax-nav { + position: relative; + z-index: 1; + padding: 0 .75rem; +} + +.ax-nav__list { + display: flex; + flex-direction: column; + gap: .125rem; +} + +.ax-nav__item { position: relative; } + +.ax-nav__link { + display: flex; + align-items: center; + gap: .625rem; + padding: .55rem .875rem; + border-radius: var(--radius-md); + font-size: .8125rem; + font-weight: 500; + color: var(--ax-muted); + transition: background var(--transition), color var(--transition); + position: relative; +} + +.ax-nav__link::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%) scaleY(0); + width: 3px; + height: 60%; + background: var(--ax-cyan); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + transition: transform var(--transition); + border-radius: var(--radius-full); +} + +.ax-nav__link:hover { + background: var(--ax-surface-2); + color: var(--ax-text); + opacity: 1; +} + +.ax-nav__link.is-active { + background: rgba(34, 211, 238, .08); + color: var(--ax-cyan); + font-weight: 600; +} +.ax-nav__link.is-active::before { transform: translateY(-50%) scaleY(1); } + +/* -------------------------------------------------------------------------- + 6. Sidebar section blocks + -------------------------------------------------------------------------- */ +.ax-sidebar-section { + position: relative; + z-index: 1; + padding: 1rem 1.25rem; + border-top: 1px solid var(--ax-border); +} + +.ax-sidebar-label { + display: block; + font-size: .6875rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ax-subtle); + margin-bottom: .625rem; +} + +.ax-sidebar-text { + font-size: .75rem; + color: var(--ax-muted); + line-height: 1.55; +} + +/* -------------------------------------------------------------------------- + 7. Contact list + -------------------------------------------------------------------------- */ +.ax-contact-list { + display: flex; + flex-direction: column; + gap: .5rem; +} + +.ax-contact-list__item { + display: flex; + align-items: flex-start; + gap: .5rem; + font-size: .75rem; + color: var(--ax-muted); + word-break: break-all; +} +.ax-contact-list__item a { + color: var(--ax-muted); + transition: color var(--transition); + word-break: break-all; +} +.ax-contact-list__item a:hover { color: var(--ax-cyan); opacity: 1; } + +.ax-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + margin-top: 1px; + color: var(--ax-cyan); +} + +/* -------------------------------------------------------------------------- + 8. Skill pills + -------------------------------------------------------------------------- */ +.ax-skill-pills { + display: flex; + flex-wrap: wrap; + gap: .35rem; +} + +.ax-skill-group-label { + width: 100%; + font-size: .625rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--ax-subtle); + margin-top: .5rem; + margin-bottom: .15rem; +} +.ax-skill-group-label:first-child { margin-top: 0; } + +.ax-skill-pill { + display: inline-block; + padding: .2rem .6rem; + background: var(--ax-surface-2); + border: 1px solid var(--ax-border-light); + border-radius: var(--radius-full); + font-size: .65rem; + font-weight: 500; + color: var(--ax-muted); + line-height: 1.5; + transition: border-color var(--transition), color var(--transition), background var(--transition); + white-space: nowrap; +} +.ax-skill-pill:hover { + border-color: var(--ax-cyan); + color: var(--ax-cyan); + background: rgba(34,211,238,.06); +} + +/* -------------------------------------------------------------------------- + 9. Sidebar action buttons + -------------------------------------------------------------------------- */ +.ax-sidebar-section--actions { + display: flex; + flex-direction: column; + gap: .625rem; + margin-top: auto; +} + +.ax-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .5rem; + padding: .6rem 1rem; + border-radius: var(--radius-md); + font-family: inherit; + font-size: .8125rem; + font-weight: 600; + cursor: pointer; + border: 1px solid transparent; + transition: background var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition); + text-align: center; + width: 100%; +} +.ax-btn .ax-icon { width: 15px; height: 15px; } + +.ax-btn--primary { + background: var(--ax-cyan); + color: #0b0d12; + border-color: var(--ax-cyan); +} +.ax-btn--primary:hover { + background: #38e4f5; + border-color: #38e4f5; + box-shadow: 0 0 12px rgba(34,211,238,.4); + color: #0b0d12; + opacity: 1; +} + +.ax-btn--ghost { + background: transparent; + color: var(--ax-muted); + border-color: var(--ax-border-light); +} +.ax-btn--ghost:hover { + color: var(--ax-text); + border-color: var(--ax-muted); + background: var(--ax-surface-2); + opacity: 1; +} + +/* -------------------------------------------------------------------------- + 10. Mobile hamburger toggle + -------------------------------------------------------------------------- */ +.ax-mobile-toggle { + display: none; + position: fixed; + top: 1rem; + left: 1rem; + z-index: 200; + width: 40px; + height: 40px; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-md); + cursor: pointer; + padding: 0; + transition: background var(--transition), border-color var(--transition); +} +.ax-mobile-toggle:hover { + background: var(--ax-surface-2); + border-color: var(--ax-border-light); +} + +.ax-mobile-toggle span { + display: block; + width: 20px; + height: 2px; + background: var(--ax-text); + border-radius: var(--radius-full); + transform-origin: center; + transition: transform var(--transition), opacity var(--transition), width var(--transition); +} + +/* X state when open */ +.ax-mobile-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); } +.ax-mobile-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; width: 0; } +.ax-mobile-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + +/* -------------------------------------------------------------------------- + 11. Main content area + -------------------------------------------------------------------------- */ +.ax-main { + margin-left: var(--sidebar-w); + flex: 1; + min-width: 0; +} + +.ax-content-wrap { + max-width: 860px; + padding: 3rem 3rem 4rem; + margin: 0 auto; +} + +/* -------------------------------------------------------------------------- + 12. Section base + -------------------------------------------------------------------------- */ +.ax-section { + padding-bottom: 3.5rem; + border-bottom: 1px solid var(--ax-border); + margin-bottom: 3.5rem; +} +.ax-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.ax-section__head { margin-bottom: 1.5rem; } + +.ax-section__eyebrow { + display: block; + font-size: .6875rem; + font-weight: 700; + letter-spacing: .12em; + text-transform: uppercase; + color: var(--ax-cyan); + margin-bottom: .4rem; +} + +.ax-section__title { + font-size: 1.375rem; + font-weight: 700; + color: var(--ax-text); + line-height: 1.3; +} + +.ax-lead { + font-size: .9375rem; + color: var(--ax-muted); + line-height: 1.75; + max-width: 72ch; +} + +/* -------------------------------------------------------------------------- + 13. Metrics grid (5 columns) + -------------------------------------------------------------------------- */ +.ax-metrics { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: .75rem; + margin-top: 1.75rem; +} + +.ax-metric-card { + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-lg); + padding: 1rem .875rem; + display: flex; + flex-direction: column; + gap: .35rem; + transition: border-color var(--transition), box-shadow var(--transition); +} +.ax-metric-card:hover { + border-color: var(--ax-border-light); + box-shadow: var(--shadow-card); +} + +.ax-metric-card__value { + font-size: 1.25rem; + font-weight: 700; + color: var(--ax-gold); + line-height: 1; + letter-spacing: -.02em; +} + +.ax-metric-card__label { + font-size: .6875rem; + color: var(--ax-muted); + line-height: 1.45; + font-weight: 400; +} + +/* -------------------------------------------------------------------------- + 14. Highlights 2×2 + -------------------------------------------------------------------------- */ +.ax-highlights { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin-bottom: 1.75rem; +} + +.ax-highlight-card { + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-lg); + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: .4rem; + transition: border-color var(--transition); +} +.ax-highlight-card:hover { border-color: var(--ax-cyan-dim); } + +.ax-highlight-card__value { + font-size: 1.875rem; + font-weight: 700; + color: var(--ax-cyan); + letter-spacing: -.03em; + line-height: 1; +} + +.ax-highlight-card__label { + font-size: .8125rem; + color: var(--ax-muted); + line-height: 1.5; +} + +/* -------------------------------------------------------------------------- + 15. Impact cards (3 columns) + -------------------------------------------------------------------------- */ +.ax-impact-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +.ax-impact-card { + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-left: 3px solid var(--ax-cyan); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + padding: 1rem 1.125rem; + transition: border-left-color var(--transition), box-shadow var(--transition); +} +.ax-impact-card:hover { + border-left-color: #38e4f5; + box-shadow: var(--shadow-card); +} + +.ax-impact-card__title { + font-size: .875rem; + font-weight: 700; + color: var(--ax-text); + margin-bottom: .4rem; +} + +.ax-impact-card__body { + font-size: .8rem; + color: var(--ax-muted); + line-height: 1.6; +} + +/* -------------------------------------------------------------------------- + 16. Timeline (experience) + -------------------------------------------------------------------------- */ +.ax-timeline { + display: flex; + flex-direction: column; + gap: 0; + position: relative; +} + +.ax-timeline__item { + display: grid; + grid-template-columns: 180px 1fr; + gap: 0 1.5rem; + position: relative; + padding-bottom: 2.5rem; +} +.ax-timeline__item:last-child { padding-bottom: 0; } + +/* vertical line along the dot column */ +.ax-timeline__item:not(:last-child)::after { + content: ''; + position: absolute; + left: calc(180px + 1.5rem / 2 - 1px); + top: 8px; + bottom: 0; + width: 1px; + background: var(--ax-border); +} + +.ax-timeline__period { + text-align: right; + padding-top: 2px; + position: relative; + display: flex; + align-items: flex-start; + justify-content: flex-end; + gap: .75rem; +} + +.ax-timeline__period-text { + font-size: .75rem; + font-weight: 600; + color: var(--ax-subtle); + line-height: 1.4; + letter-spacing: .01em; +} + +.ax-timeline__dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--ax-surface); + border: 2px solid var(--ax-cyan); + flex-shrink: 0; + margin-top: 3px; + position: relative; + z-index: 1; + transition: background var(--transition), box-shadow var(--transition); +} +.ax-timeline__item:hover .ax-timeline__dot { + background: var(--ax-cyan); + box-shadow: 0 0 8px rgba(34,211,238,.5); +} + +.ax-timeline__content { padding-top: 0; } + +.ax-timeline__job-title { + font-size: 1rem; + font-weight: 700; + color: var(--ax-text); + margin-bottom: .2rem; +} + +.ax-timeline__meta { + font-size: .8125rem; + color: var(--ax-muted); + margin-bottom: .5rem; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: .25rem; +} + +.ax-timeline__sep { + color: var(--ax-subtle); +} + +.ax-timeline__lead { + font-size: .8375rem; + color: var(--ax-muted); + line-height: 1.65; + margin-bottom: .75rem; + font-style: italic; +} + +.ax-timeline__bullets { + display: flex; + flex-direction: column; + gap: .375rem; +} + +.ax-timeline__bullets li { + font-size: .8125rem; + color: var(--ax-muted); + line-height: 1.6; + padding-left: 1.1rem; + position: relative; +} +.ax-timeline__bullets li::before { + content: '›'; + position: absolute; + left: 0; + top: 0; + color: var(--ax-cyan); + font-weight: 700; + line-height: 1.6; +} + +/* -------------------------------------------------------------------------- + 17. Education cards + -------------------------------------------------------------------------- */ +.ax-edu-cards { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.ax-edu-card { + display: flex; + align-items: flex-start; + gap: 1.25rem; + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-lg); + padding: 1.25rem 1.5rem; + transition: border-color var(--transition), box-shadow var(--transition); +} +.ax-edu-card:hover { + border-color: var(--ax-border-light); + box-shadow: var(--shadow-card); +} + +.ax-edu-card__year { + flex-shrink: 0; + font-size: 1.125rem; + font-weight: 700; + color: var(--ax-gold); + letter-spacing: -.02em; + min-width: 48px; + padding-top: 2px; +} + +.ax-edu-card__title { + font-size: .9375rem; + font-weight: 700; + color: var(--ax-text); + margin-bottom: .3rem; +} + +.ax-edu-card__institution { + font-size: .8125rem; + color: var(--ax-muted); + margin-bottom: .25rem; +} + +.ax-edu-card__detail { + font-size: .75rem; + color: var(--ax-subtle); + font-style: italic; +} + +/* -------------------------------------------------------------------------- + 18. Certifications list + -------------------------------------------------------------------------- */ +.ax-cert-list { + display: flex; + flex-direction: column; + gap: .625rem; +} + +.ax-cert-item { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: .875rem 1.125rem; + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-md); + transition: border-color var(--transition); +} +.ax-cert-item:hover { border-color: var(--ax-border-light); } + +.ax-cert-item__year { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + padding: .15rem .6rem; + background: rgba(34,211,238,.1); + border: 1px solid var(--ax-cyan-dim); + border-radius: var(--radius-full); + font-size: .6875rem; + font-weight: 700; + color: var(--ax-cyan); + letter-spacing: .04em; + white-space: nowrap; + margin-top: 2px; +} + +.ax-cert-item__title { + font-size: .875rem; + font-weight: 600; + color: var(--ax-text); + margin-bottom: .2rem; + line-height: 1.35; +} + +.ax-cert-item__issuer { + font-size: .75rem; + color: var(--ax-muted); + line-height: 1.45; +} + +/* -------------------------------------------------------------------------- + 19. Extras grid (contacto section) + -------------------------------------------------------------------------- */ +.ax-extras-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.ax-extras-grid__card--bcard { + grid-column: 1 / -1; +} + +.ax-extras-grid__card { + background: var(--ax-surface); + border: 1px solid var(--ax-border); + border-radius: var(--radius-lg); + padding: 1.25rem 1.5rem; +} + +.ax-extras__heading { + font-size: .9375rem; + font-weight: 700; + color: var(--ax-text); + margin-bottom: .2rem; + margin-top: .125rem; +} + +.ax-extras__year { + font-size: .75rem; + font-weight: 700; + color: var(--ax-gold); + margin-bottom: .5rem; +} + +.ax-extras__body { + font-size: .8125rem; + color: var(--ax-muted); + line-height: 1.6; +} + +/* -------------------------------------------------------------------------- + 20. Data list (additional info) + -------------------------------------------------------------------------- */ +.ax-data-list { + display: flex; + flex-direction: column; + gap: .5rem; + margin-top: .25rem; +} + +.ax-data-list__row { + display: flex; + flex-wrap: wrap; + gap: .25rem .5rem; +} + +.ax-data-list dt { + font-size: .75rem; + font-weight: 700; + color: var(--ax-subtle); + min-width: 120px; + flex-shrink: 0; +} + +.ax-data-list dd { + font-size: .8rem; + color: var(--ax-muted); + flex: 1; + min-width: 120px; + line-height: 1.5; +} + +/* -------------------------------------------------------------------------- + 21. Business card + -------------------------------------------------------------------------- */ +.ax-bcard { + background: linear-gradient(135deg, var(--ax-surface-2) 0%, var(--ax-surface-3) 100%); + border: 1px solid var(--ax-border-light); + border-radius: var(--radius-lg); + overflow: hidden; + margin-top: .75rem; +} + +.ax-bcard__header { + padding: 1rem 1.25rem .875rem; + border-bottom: 1px solid var(--ax-border); + background: rgba(34,211,238,.04); +} + +.ax-bcard__name { + font-size: .9375rem; + font-weight: 700; + color: var(--ax-text); + margin-bottom: .2rem; +} + +.ax-bcard__role { + font-size: .75rem; + color: var(--ax-cyan); + font-weight: 500; +} + +.ax-bcard__body { + display: flex; + gap: 1rem; + padding: 1rem 1.25rem; + align-items: center; +} + +.ax-bcard__qr { + flex-shrink: 0; + padding: .375rem; + background: #fff; + border-radius: var(--radius-sm); +} +.ax-bcard__qr img { + display: block; + width: 100px; + height: 100px; + border-radius: 2px; +} + +.ax-bcard__info { + display: flex; + flex-direction: column; + gap: .35rem; +} +.ax-bcard__info p, .ax-bcard__info a { + font-size: .75rem; + color: var(--ax-muted); + line-height: 1.45; +} +.ax-bcard__info a:hover { color: var(--ax-cyan); opacity: 1; } + +/* -------------------------------------------------------------------------- + 22. Sidebar overlay (mobile backdrop) + -------------------------------------------------------------------------- */ +.ax-sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,.65); + z-index: 99; + opacity: 0; + transition: opacity 200ms ease; +} +.ax-sidebar-overlay.is-visible { + display: block; + opacity: 1; +} + +/* -------------------------------------------------------------------------- + 23. Responsive — 1024px: narrower sidebar, 3-col metrics, flat timeline + -------------------------------------------------------------------------- */ +@media (max-width: 1024px) { + .ax-sidebar { width: var(--sidebar-w-sm); } + .ax-main { margin-left: var(--sidebar-w-sm); } + + .ax-content-wrap { padding: 2.5rem 2rem 3rem; } + + .ax-metrics { + grid-template-columns: repeat(3, 1fr); + } + + .ax-impact-cards { + grid-template-columns: 1fr 1fr; + } + + /* Flatten timeline: full-width blocks stacked */ + .ax-timeline__item { + grid-template-columns: 1fr; + padding-left: 1.25rem; + padding-bottom: 2rem; + } + .ax-timeline__item::after { + left: 0; + top: 14px; + } + + .ax-timeline__period { + text-align: left; + justify-content: flex-start; + flex-direction: row-reverse; + margin-bottom: .5rem; + } + .ax-timeline__dot { + position: absolute; + left: -5px; + top: 4px; + margin-top: 0; + } +} + +/* -------------------------------------------------------------------------- + 24. Responsive — 768px: mobile sidebar + -------------------------------------------------------------------------- */ +@media (max-width: 768px) { + .ax-mobile-toggle { display: flex; } + + .ax-sidebar { + transform: translateX(-100%); + box-shadow: none; + width: var(--sidebar-w); + } + .ax-sidebar.is-open { + transform: translateX(0); + box-shadow: var(--shadow-raised); + } + + .ax-main { margin-left: 0; } + + .ax-content-wrap { + padding: 5rem 1.25rem 3rem; + max-width: 100%; + } + + .ax-metrics { + grid-template-columns: repeat(2, 1fr); + } + + .ax-highlights { + grid-template-columns: 1fr; + } + + .ax-impact-cards { + grid-template-columns: 1fr; + } + + .ax-section__title { font-size: 1.125rem; } + + .ax-highlight-card__value { font-size: 1.5rem; } + + .ax-extras-grid { + grid-template-columns: 1fr; + } + .ax-extras-grid__card--bcard { + grid-column: 1; + } +} + +@media (max-width: 480px) { + .ax-metrics { grid-template-columns: 1fr 1fr; } + + .ax-bcard__body { flex-direction: column; } + + .ax-timeline__item { + padding-left: 1rem; + } +} + +/* -------------------------------------------------------------------------- + 25. Print styles + -------------------------------------------------------------------------- */ +@media print { + :root { + --ax-bg: #fff; + --ax-surface: #f8f8f8; + --ax-surface-2: #f0f0f0; + --ax-surface-3: #e8e8e8; + --ax-border: #ddd; + --ax-border-light: #ccc; + --ax-text: #111; + --ax-muted: #444; + --ax-subtle: #666; + --ax-cyan: #0077aa; + --ax-cyan-dim: #0077aa; + --ax-gold: #c07800; + --ax-green: #007700; + } + + body { background: #fff; color: #111; } + + .ax-mobile-toggle { display: none !important; } + + .ax-layout { display: block; } + + .ax-sidebar { + position: static; + width: 100%; + border: none; + border-bottom: 2px solid #ccc; + overflow: visible; + transform: none !important; + animation: none !important; + page-break-after: avoid; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: .5rem 1rem; + padding: 1rem; + } + + .ax-sidebar::before { display: none; } + + .ax-profile { + grid-column: 1; + text-align: left; + align-items: flex-start; + padding: 0; + } + + .ax-profile__avatar { + width: 64px; + height: 64px; + animation: none; + box-shadow: none; + } + .ax-profile__status { display: none; } + + .ax-nav { grid-column: 2; } + .ax-nav__list { flex-direction: row; flex-wrap: wrap; gap: .25rem; } + .ax-nav__link { color: #333; font-size: .75rem; padding: .25rem .5rem; } + .ax-nav__link.is-active { color: var(--ax-cyan); } + .ax-nav__link::before { display: none; } + + .ax-sidebar-section { grid-column: 3; border: none; padding: 0; } + .ax-sidebar-section--actions { display: none; } + + .ax-skill-pill { + background: #eee; + border-color: #ccc; + color: #333; + } + + .ax-main { margin-left: 0; } + + .ax-content-wrap { max-width: 100%; padding: 1.5rem 0 0; } + + .ax-section { page-break-inside: avoid; } + + .ax-metric-card, + .ax-highlight-card, + .ax-impact-card, + .ax-edu-card, + .ax-cert-item { + background: #f8f8f8; + border-color: #ddd; + box-shadow: none; + } + + .ax-metrics { grid-template-columns: repeat(5, 1fr); } + .ax-highlights { grid-template-columns: repeat(2, 1fr); } + .ax-impact-cards { grid-template-columns: repeat(3, 1fr); } + + .ax-timeline__item::after { background: #ccc; } + + a { color: inherit; text-decoration: none; } + a[href]::after { content: none; } +} diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/docs/CV_Patricia_Flores_Sanchez_ES_FINAL.docx b/backend/wordpress/wp-content/themes/apex-cv/assets/docs/CV_Patricia_Flores_Sanchez_ES_FINAL.docx new file mode 100644 index 0000000..544bf3d Binary files /dev/null and b/backend/wordpress/wp-content/themes/apex-cv/assets/docs/CV_Patricia_Flores_Sanchez_ES_FINAL.docx differ diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-profile.jpeg b/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-profile.jpeg new file mode 100644 index 0000000..4151a20 Binary files /dev/null and b/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-profile.jpeg differ diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-qr.png b/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-qr.png new file mode 100644 index 0000000..55703ad Binary files /dev/null and b/backend/wordpress/wp-content/themes/apex-cv/assets/images/patricia-flores-qr.png differ diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/images/profile-placeholder.svg b/backend/wordpress/wp-content/themes/apex-cv/assets/images/profile-placeholder.svg new file mode 100644 index 0000000..3d7dbb0 --- /dev/null +++ b/backend/wordpress/wp-content/themes/apex-cv/assets/images/profile-placeholder.svg @@ -0,0 +1,10 @@ + diff --git a/backend/wordpress/wp-content/themes/apex-cv/assets/js/theme.js b/backend/wordpress/wp-content/themes/apex-cv/assets/js/theme.js new file mode 100644 index 0000000..5c4fe90 --- /dev/null +++ b/backend/wordpress/wp-content/themes/apex-cv/assets/js/theme.js @@ -0,0 +1,183 @@ +/** + * Apex CV — theme.js + * Mobile sidebar toggle · Print button · Scroll spy + */ +(function () { + 'use strict'; + + /* ----------------------------------------------------------------------- + Helpers + ----------------------------------------------------------------------- */ + function qs(selector, ctx) { + return (ctx || document).querySelector(selector); + } + function qsa(selector, ctx) { + return Array.from((ctx || document).querySelectorAll(selector)); + } + + /* ----------------------------------------------------------------------- + Mobile sidebar + ----------------------------------------------------------------------- */ + var sidebar = qs('#ax-sidebar'); + var toggle = qs('.ax-mobile-toggle'); + var overlay = null; + + function createOverlay() { + if (overlay) return overlay; + overlay = document.createElement('div'); + overlay.className = 'ax-sidebar-overlay'; + overlay.setAttribute('aria-hidden', 'true'); + document.body.appendChild(overlay); + overlay.addEventListener('click', closeSidebar); + return overlay; + } + + function openSidebar() { + if (!sidebar || !toggle) return; + sidebar.classList.add('is-open'); + toggle.setAttribute('aria-expanded', 'true'); + var ov = createOverlay(); + // force reflow before adding class for transition + ov.offsetHeight; // eslint-disable-line no-unused-expressions + ov.classList.add('is-visible'); + document.body.style.overflow = 'hidden'; + } + + function closeSidebar() { + if (!sidebar || !toggle) return; + sidebar.classList.remove('is-open'); + toggle.setAttribute('aria-expanded', 'false'); + if (overlay) overlay.classList.remove('is-visible'); + document.body.style.overflow = ''; + } + + function isMobile() { + return window.innerWidth <= 768; + } + + if (toggle) { + toggle.addEventListener('click', function () { + var isOpen = sidebar && sidebar.classList.contains('is-open'); + if (isOpen) { + closeSidebar(); + } else { + openSidebar(); + } + }); + } + + /* Close sidebar when clicking outside on mobile */ + document.addEventListener('click', function (e) { + if (!isMobile()) return; + if (!sidebar || !sidebar.classList.contains('is-open')) return; + if (sidebar.contains(e.target)) return; + if (toggle && toggle.contains(e.target)) return; + closeSidebar(); + }); + + /* Close sidebar on nav link click (mobile) */ + qsa('.ax-nav__link').forEach(function (link) { + link.addEventListener('click', function () { + if (isMobile()) { + closeSidebar(); + } + }); + }); + + /* Close sidebar on resize back to desktop */ + window.addEventListener('resize', function () { + if (!isMobile() && sidebar && sidebar.classList.contains('is-open')) { + closeSidebar(); + } + }); + + /* ----------------------------------------------------------------------- + Print button + ----------------------------------------------------------------------- */ + qsa('[data-apex-print]').forEach(function (btn) { + btn.addEventListener('click', function () { + window.print(); + }); + }); + + /* ----------------------------------------------------------------------- + Scroll spy via IntersectionObserver + ----------------------------------------------------------------------- */ + var navLinks = qsa('.ax-nav__link'); + + if ('IntersectionObserver' in window && navLinks.length > 0) { + var activeSection = null; + + function setActive(sectionId) { + if (activeSection === sectionId) return; + activeSection = sectionId; + navLinks.forEach(function (link) { + var target = link.getAttribute('data-section') || link.getAttribute('href').replace('#', ''); + if (target === sectionId) { + link.classList.add('is-active'); + link.setAttribute('aria-current', 'true'); + } else { + link.classList.remove('is-active'); + link.removeAttribute('aria-current'); + } + }); + } + + // Collect sections that correspond to nav links + var sectionIds = navLinks.map(function (link) { + return link.getAttribute('data-section') || link.getAttribute('href').replace('#', ''); + }); + + var sectionEls = sectionIds + .map(function (id) { return document.getElementById(id); }) + .filter(Boolean); + + // Track which sections are currently intersecting + var visibleSections = new Set(); + + var observer = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + visibleSections.add(entry.target.id); + } else { + visibleSections.delete(entry.target.id); + } + }); + + // Activate the topmost visible section (matches reading order) + var toActivate = null; + sectionEls.forEach(function (el) { + if (visibleSections.has(el.id) && toActivate === null) { + toActivate = el.id; + } + }); + + if (toActivate) { + setActive(toActivate); + } + }, + { + rootMargin: '-10% 0px -60% 0px', + threshold: 0 + } + ); + + sectionEls.forEach(function (el) { observer.observe(el); }); + } + + /* ----------------------------------------------------------------------- + Smooth scroll for anchor links (fallback for older browsers) + ----------------------------------------------------------------------- */ + qsa('a[href^="#"]').forEach(function (link) { + link.addEventListener('click', function (e) { + var href = link.getAttribute('href'); + if (!href || href === '#') return; + var target = document.querySelector(href); + if (!target) return; + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }); + +})(); diff --git a/backend/wordpress/wp-content/themes/apex-cv/footer.php b/backend/wordpress/wp-content/themes/apex-cv/footer.php new file mode 100644 index 0000000..4acbb15 --- /dev/null +++ b/backend/wordpress/wp-content/themes/apex-cv/footer.php @@ -0,0 +1,5 @@ + +