From 4b280fb9dad6758e65b3cbe2819f2bab3a4936d5 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Sun, 16 Aug 2026 09:45:37 -0500 Subject: [PATCH 1/9] Formularios de speackers y registro --- 2026/css/style.css | 129 +++++ 2026/css/variables.css | 15 + 2026/form/README.md | 9 + 2026/form/config.example.js | 5 + 2026/form/index.html | 762 +++++++++++++++++++++++++ 2026/form/js/form-submit.js | 68 +++ 2026/form/registro.html | 91 +++ 2026/form/speaker.html | 93 +++ 2026/index.html | 10 + 2026_dev/public/form/README.md | 9 + 2026_dev/public/form/config.example.js | 5 + 2026_dev/public/form/js/form-submit.js | 65 +++ 2026_dev/public/form/registro.html | 42 ++ 2026_dev/public/form/speaker.html | 49 ++ 14 files changed, 1352 insertions(+) create mode 100644 2026/form/README.md create mode 100644 2026/form/config.example.js create mode 100644 2026/form/index.html create mode 100644 2026/form/js/form-submit.js create mode 100644 2026/form/registro.html create mode 100644 2026/form/speaker.html create mode 100644 2026_dev/public/form/README.md create mode 100644 2026_dev/public/form/config.example.js create mode 100644 2026_dev/public/form/js/form-submit.js create mode 100644 2026_dev/public/form/registro.html create mode 100644 2026_dev/public/form/speaker.html diff --git a/2026/css/style.css b/2026/css/style.css index bbcab7f..b1748dd 100644 --- a/2026/css/style.css +++ b/2026/css/style.css @@ -240,6 +240,135 @@ background: linear-gradient(180deg, var(--accent-soft) 0%, transparent 100%); } + +/* ========================================================================== + Forms + ========================================================================== */ + +.container form { + max-width: 820px; + margin: 0 auto; +} + +.form-card { + background: var(--bg); + border-radius: var(--radius-lg); + padding: var(--space-5); + box-shadow: 0 8px 24px rgba(2,6,23,0.06); + border: 1px solid rgba(17,24,39,0.04); +} + +label { + display: block; + margin-bottom: var(--space-3); + font-size: var(--fs-sm); + color: var(--text-secondary); +} + +input[type="text"], +input[type="email"], +input[type="tel"], +input[type="number"], +select, +textarea { + width: 100%; + background: var(--input-bg); + border: 1px solid var(--input-border); + padding: var(--input-padding); + border-radius: var(--input-radius); + font-size: var(--fs-base); + color: var(--text); + box-shadow: none; + transition: box-shadow var(--transition), border-color var(--transition); +} + +textarea { resize: vertical; min-height: 120px; max-height: 400px; } + +input:focus, select:focus, textarea:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(47,140,255,0.12); + border-color: var(--accent); +} + +/* Required field visual hint */ +input:required, textarea:required, select:required { + box-shadow: inset 0 -1px 0 rgba(0,0,0,0.02); +} + +input:invalid, textarea:invalid { + border-color: var(--color-error); +} + +/* Buttons */ +.btn { padding: var(--btn-padding); border-radius: var(--btn-radius); display: inline-flex; align-items: center; gap: 8px; } +.btn-primary { background: var(--btn-primary-bg); color: var(--btn-primary-color); border: 1px solid transparent; } +.btn-primary:hover { filter: brightness(0.96); } +.btn-secondary { background: var(--btn-secondary-bg); color: var(--btn-secondary-color); border: 1px solid rgba(17,24,39,0.06); } + +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); } +@media (max-width: 720px) { .form-row { grid-template-columns: 1fr; } } + +label { font-weight: 600; } + +.form-note { font-size: 0.9rem; color: var(--text-secondary); margin-top: calc(var(--space-2) * -1); } + +/* Form utilities inspired by python_panama_website (Bootstrap-like) */ +.form-group { margin-bottom: var(--space-4); } +.form-label { display: block; margin-bottom: var(--space-2); font-weight: 700; } +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 1rem); + padding: 0.5rem 0.75rem; + font-size: var(--fs-base); + line-height: 1.5; + color: var(--text); + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: calc(var(--input-radius) - 2px); +} +.form-control::placeholder { color: var(--text-secondary); } +.form-control:focus { box-shadow: 0 0 0 4px rgba(47,140,255,0.12); border-color: var(--accent); } +.form-select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--text-secondary) 50%), linear-gradient(135deg, var(--text-secondary) 50%, transparent 50%); background-position: right .75rem center, right .5rem center; background-size: .45rem .45rem, .45rem .45rem; background-repeat: no-repeat; padding-right: 2.5rem; } + +.form-check { display: flex; align-items: flex-start; gap: var(--space-3); margin-bottom: var(--space-3); } +.form-check-input { margin-top: .2rem; width: 1.1rem; height: 1.1rem; } +.form-check-label { margin: 0; } + +.alert { padding: var(--space-3); border-radius: var(--radius-sm); margin-bottom: var(--space-4); } +.alert-success { background: rgba(16,185,129,0.06); color: var(--color-success); border-left: 4px solid var(--color-success); } +.alert-danger { background: rgba(220,38,38,0.04); color: var(--color-error); border-left: 4px solid var(--color-error); } + +.btn-lg { padding: 14px 22px; font-size: 1.05rem; } +.btn-outline-primary { background: transparent; border: 1px solid rgba(47,140,255,0.12); color: var(--accent); } + +.form-info-box { display: flex; gap: var(--space-3); align-items: flex-start; margin-top: var(--space-4); padding: var(--space-3); border-radius: var(--radius-sm); background: rgba(47,140,255,0.03); } +.form-info-box i { font-size: 1.2rem; color: var(--accent); } +.data-protection-box { padding: var(--space-3); background: rgba(0,0,0,0.02); border-radius: var(--radius-sm); margin-bottom: var(--space-3); } + +.me-2 { margin-right: .5rem; } + +button[type="submit"], .btn { + cursor: pointer; +} + +.form-message { + margin-top: var(--space-4); + padding: var(--space-3); + border-radius: var(--radius-sm); + background: var(--form-message-bg); + color: var(--text); +} + +.form-message.error { border-left: 4px solid var(--color-error); color: var(--color-error); background: rgba(220,38,38,0.04); } +.form-message.success { border-left: 4px solid var(--color-success); color: var(--color-success); background: rgba(5,150,105,0.04); } + +.form-actions { display: flex; gap: var(--space-3); align-items: center; margin-top: var(--space-4); } + +@media (max-width: 600px) { + .form-actions { flex-direction: column; align-items: stretch; } +} + .hero-content { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.9fr); diff --git a/2026/css/variables.css b/2026/css/variables.css index 0160087..0ae19da 100644 --- a/2026/css/variables.css +++ b/2026/css/variables.css @@ -81,4 +81,19 @@ /* --- Foco (accesibilidad) ---------------------------------------------- */ --focus-ring: 3px solid var(--accent); --focus-offset: 3px; + /* --- Form controls ---------------------------------------------------- */ + --input-bg: #ffffff; + --input-border: rgba(17,24,39,0.08); + --input-radius: 10px; + --input-padding: 12px 14px; + --color-error: #dc2626; + --color-success: #059669; + --form-message-bg: rgba(47,140,255,0.03); + /* --- Botones ---------------------------------------------------------- */ + --btn-primary-bg: var(--accent); + --btn-primary-color: var(--text-on-accent); + --btn-secondary-bg: transparent; + --btn-secondary-color: var(--text); + --btn-padding: 10px 18px; + --btn-radius: 10px; } diff --git a/2026/form/README.md b/2026/form/README.md new file mode 100644 index 0000000..a86ffa7 --- /dev/null +++ b/2026/form/README.md @@ -0,0 +1,9 @@ +Instrucciones rápidas para integrar con Supabase + +1. En Supabase crea un proyecto y añade dos tablas: `speakers` y `registrations` con columnas que coincidan con los campos del formulario (por ejemplo: name, email, affiliation, bio, title, abstract, duration, language, links, consent para speakers; name, email, organization, role, diet, accessibility, phone, consent_photos para registrations). +2. Habilita CORS para tu dominio en Supabase. +3. Copia `config.example.js` a `config.js` y rellena `url` y `anonKey`. +4. Despliega la carpeta `public/` en tu host (Netlify/Vercel/Netlify Functions). Las páginas estarán disponibles en `/form/speaker.html` y `/form/registro.html`. +5. Verifica en Supabase que los inserts funcionan; revisa la tabla y los registros. + +Notas de seguridad: el `anonKey` es la clave pública pensada para operaciones cliente (inserciones públicas). Para acciones sensibles, crea endpoints server-side. diff --git a/2026/form/config.example.js b/2026/form/config.example.js new file mode 100644 index 0000000..ea5c75a --- /dev/null +++ b/2026/form/config.example.js @@ -0,0 +1,5 @@ +// Copia este archivo a config.js y completa los valores reales antes de desplegar. +window.SUPABASE_CONFIG = { + url: 'https://your-project.supabase.co', + anonKey: 'YOUR_ANON_KEY' +}; diff --git a/2026/form/index.html b/2026/form/index.html new file mode 100644 index 0000000..b414adc --- /dev/null +++ b/2026/form/index.html @@ -0,0 +1,762 @@ + + + + + + + Registro al evento + + + + + + + + + + + + + + + +
+ + +
+
+ +
+ +
+ +
+ +
+ +
+ PyCon + Panamá + 2025 +
+ +
+ +
+ InicioAcerca de. + AgendaSedesSponsors +
+
+
+ +
+ +
+ +
PyCon Panamá 2025
+ + +
+
+
+ +
+ +
+ + + + + + + +
+ + +
+
+ Registro Formulario + +
+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/2026/form/js/form-submit.js b/2026/form/js/form-submit.js new file mode 100644 index 0000000..729f003 --- /dev/null +++ b/2026/form/js/form-submit.js @@ -0,0 +1,68 @@ +(function(){ + function getConfig(){ + return window.SUPABASE_CONFIG || null; + } + + async function submitToSupabase(table, payload){ + const cfg = getConfig(); + if(!cfg || !cfg.url || !cfg.anonKey){ + throw new Error('Falta configuración de Supabase. Crea /form/config.js con las credenciales.'); + } + const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': cfg.anonKey, + 'Authorization': 'Bearer ' + cfg.anonKey, + 'Prefer': 'return=representation' + }, + body: JSON.stringify(payload) + }); + if(!res.ok){ + const text = await res.text(); + throw new Error('Error al enviar: ' + res.status + ' ' + text); + } + return res.json(); + } + + function serializeForm(form){ + const data = {}; + const fm = new FormData(form); + for(const [k,v] of fm.entries()){ + // Normalize checkbox values + if(typeof v === 'string'){ + data[k] = v; + } + } + return data; + } + + function attach(formId){ + const form = document.getElementById(formId); + if(!form) return; + const message = form.querySelector('#form-message'); + form.addEventListener('submit', async (ev) =>{ + ev.preventDefault(); + message.className = 'alert'; + message.textContent = 'Enviando...'; + const table = form.dataset.table; + const payload = serializeForm(form); + try{ + await submitToSupabase(table, payload); + message.className = 'alert alert-success'; + message.textContent = 'Enviado correctamente. Gracias.'; + form.reset(); + }catch(e){ + console.error(e); + message.className = 'alert alert-danger'; + message.textContent = 'Error enviando formulario. Ver consola.'; + } + }); + } + + document.addEventListener('DOMContentLoaded', () =>{ + attach('speaker-form'); + attach('registro-form'); + }); +})(); \ No newline at end of file diff --git a/2026/form/registro.html b/2026/form/registro.html new file mode 100644 index 0000000..34a1dcf --- /dev/null +++ b/2026/form/registro.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + +
+

Registro de asistentes

+

Regístrate para asistir al evento.

+
+
+ + + + + + + +
+ + +
+
+ +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/2026/form/speaker.html b/2026/form/speaker.html new file mode 100644 index 0000000..dfd1c5f --- /dev/null +++ b/2026/form/speaker.html @@ -0,0 +1,93 @@ + + + + + + Proponer charla - PyCon Panamá 2026 + + + + + + + + + + + + + +
+

Proponer ponencia

+

Completa el formulario para enviar tu propuesta de charla.

+
+
+ + + + + + + + + +
+ + +
+
+ +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/2026/index.html b/2026/index.html index 55a46f7..6b01dac 100644 --- a/2026/index.html +++ b/2026/index.html @@ -63,6 +63,8 @@

PyCon Panamá 2026

Ver agenda Conoce el evento + Proponer ponencia + Registrarme
@@ -90,6 +92,14 @@

Agenda

Preguntas frecuentes

Asistencia, ponencias y patrocinio.

+ +

Proponer ponencia

+

Envía tu propuesta para ser considerado en la agenda.

+
+ +

Registro

+

Regístrate para asistir al evento.

+
diff --git a/2026_dev/public/form/README.md b/2026_dev/public/form/README.md new file mode 100644 index 0000000..a86ffa7 --- /dev/null +++ b/2026_dev/public/form/README.md @@ -0,0 +1,9 @@ +Instrucciones rápidas para integrar con Supabase + +1. En Supabase crea un proyecto y añade dos tablas: `speakers` y `registrations` con columnas que coincidan con los campos del formulario (por ejemplo: name, email, affiliation, bio, title, abstract, duration, language, links, consent para speakers; name, email, organization, role, diet, accessibility, phone, consent_photos para registrations). +2. Habilita CORS para tu dominio en Supabase. +3. Copia `config.example.js` a `config.js` y rellena `url` y `anonKey`. +4. Despliega la carpeta `public/` en tu host (Netlify/Vercel/Netlify Functions). Las páginas estarán disponibles en `/form/speaker.html` y `/form/registro.html`. +5. Verifica en Supabase que los inserts funcionan; revisa la tabla y los registros. + +Notas de seguridad: el `anonKey` es la clave pública pensada para operaciones cliente (inserciones públicas). Para acciones sensibles, crea endpoints server-side. diff --git a/2026_dev/public/form/config.example.js b/2026_dev/public/form/config.example.js new file mode 100644 index 0000000..ea5c75a --- /dev/null +++ b/2026_dev/public/form/config.example.js @@ -0,0 +1,5 @@ +// Copia este archivo a config.js y completa los valores reales antes de desplegar. +window.SUPABASE_CONFIG = { + url: 'https://your-project.supabase.co', + anonKey: 'YOUR_ANON_KEY' +}; diff --git a/2026_dev/public/form/js/form-submit.js b/2026_dev/public/form/js/form-submit.js new file mode 100644 index 0000000..06d4868 --- /dev/null +++ b/2026_dev/public/form/js/form-submit.js @@ -0,0 +1,65 @@ +(function(){ + function getConfig(){ + return window.SUPABASE_CONFIG || null; + } + + async function submitToSupabase(table, payload){ + const cfg = getConfig(); + if(!cfg || !cfg.url || !cfg.anonKey){ + throw new Error('Falta configuración de Supabase. Crea /form/config.js con las credenciales.'); + } + const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': cfg.anonKey, + 'Authorization': 'Bearer ' + cfg.anonKey, + 'Prefer': 'return=representation' + }, + body: JSON.stringify(payload) + }); + if(!res.ok){ + const text = await res.text(); + throw new Error('Error al enviar: ' + res.status + ' ' + text); + } + return res.json(); + } + + function serializeForm(form){ + const data = {}; + const fm = new FormData(form); + for(const [k,v] of fm.entries()){ + // Normalize checkbox values + if(typeof v === 'string'){ + data[k] = v; + } + } + return data; + } + + function attach(formId){ + const form = document.getElementById(formId); + if(!form) return; + const message = form.querySelector('#form-message'); + form.addEventListener('submit', async (ev) =>{ + ev.preventDefault(); + message.textContent = 'Enviando...'; + const table = form.dataset.table; + const payload = serializeForm(form); + try{ + await submitToSupabase(table, payload); + message.textContent = 'Enviado correctamente. Gracias.'; + form.reset(); + }catch(e){ + console.error(e); + message.textContent = 'Error enviando formulario. Ver consola.'; + } + }); + } + + document.addEventListener('DOMContentLoaded', () =>{ + attach('speaker-form'); + attach('registro-form'); + }); +})(); \ No newline at end of file diff --git a/2026_dev/public/form/registro.html b/2026_dev/public/form/registro.html new file mode 100644 index 0000000..4c3f2b0 --- /dev/null +++ b/2026_dev/public/form/registro.html @@ -0,0 +1,42 @@ + + + + + + Registro - PyCon Panamá 2026 + + + + +
+

Registro de asistentes

+

Regístrate para asistir al evento.

+
+ + + + + + + + +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/2026_dev/public/form/speaker.html b/2026_dev/public/form/speaker.html new file mode 100644 index 0000000..15c4e66 --- /dev/null +++ b/2026_dev/public/form/speaker.html @@ -0,0 +1,49 @@ + + + + + + Proponer charla - PyCon Panamá 2026 + + + + +
+

Proponer ponencia

+

Completa el formulario para enviar tu propuesta de charla.

+
+ + + + + + + + + + +
+ +
+
+
+
+ + + + + \ No newline at end of file From fbcd8da1cf3634ed12c48865d809b1b3fdfe39ec Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Sun, 16 Aug 2026 13:57:21 -0500 Subject: [PATCH 2/9] credenciales --- 2026/form/config.example.js | 4 +- 2026/form/config.js | 6 +++ 2026/form/js/form-submit.js | 71 +++++++++++++++++++------- 2026/form/registro.html | 5 +- 2026_dev/.gitignore | 4 ++ 2026_dev/public/form/config.example.js | 10 +++- 2026_dev/public/form/config.js | 6 +++ 2026_dev/public/form/js/form-submit.js | 64 ++++++++++++++++------- 2026_dev/public/form/registro.html | 6 +-- 9 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 2026/form/config.js create mode 100644 2026_dev/public/form/config.js diff --git a/2026/form/config.example.js b/2026/form/config.example.js index ea5c75a..ef33551 100644 --- a/2026/form/config.example.js +++ b/2026/form/config.example.js @@ -1,5 +1,5 @@ // Copia este archivo a config.js y completa los valores reales antes de desplegar. window.SUPABASE_CONFIG = { - url: 'https://your-project.supabase.co', - anonKey: 'YOUR_ANON_KEY' + url: '--', + anonKey: '--' }; diff --git a/2026/form/config.js b/2026/form/config.js new file mode 100644 index 0000000..3c7655f --- /dev/null +++ b/2026/form/config.js @@ -0,0 +1,6 @@ +// Archivo local con configuración de Supabase para formularios. +// NO subir este archivo al repositorio. Está listado en .gitignore. +window.SUPABASE_CONFIG = { + url: 'https://wfiyucykjoohdiazlqbz.supabase.co', + anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' +}; diff --git a/2026/form/js/form-submit.js b/2026/form/js/form-submit.js index 729f003..c376104 100644 --- a/2026/form/js/form-submit.js +++ b/2026/form/js/form-submit.js @@ -5,36 +5,71 @@ async function submitToSupabase(table, payload){ const cfg = getConfig(); - if(!cfg || !cfg.url || !cfg.anonKey){ - throw new Error('Falta configuración de Supabase. Crea /form/config.js con las credenciales.'); + const secureEndpoint = cfg && cfg.secureEndpoint; + // If secureEndpoint configured, try server-side proxy first + if (secureEndpoint){ + try{ + const r = await fetch(secureEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ table, payload }) + }); + const text = await r.text(); + if(!r.ok) throw new Error('Server error: ' + r.status + ' ' + text); + try{ return JSON.parse(text); }catch(e){ return text; } + }catch(err){ + console.warn('Secure endpoint failed, falling back to client-side if configured', err); + } } - const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'apikey': cfg.anonKey, - 'Authorization': 'Bearer ' + cfg.anonKey, - 'Prefer': 'return=representation' - }, - body: JSON.stringify(payload) - }); - if(!res.ok){ - const text = await res.text(); - throw new Error('Error al enviar: ' + res.status + ' ' + text); + + // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) + if(cfg && cfg.url && cfg.anonKey){ + let url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + // Append apikey as query param as a fallback if headers are stripped by CORS/proxy + if (cfg.anonKey && url.indexOf('apikey=') === -1){ + url += (url.indexOf('?') === -1 ? '?' : '&') + 'apikey=' + encodeURIComponent(cfg.anonKey); + } + const bodyToSend = Array.isArray(payload) ? payload : [payload]; + console.log('Direct Supabase POST payload (array):', bodyToSend); + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': cfg.anonKey, + 'Authorization': 'Bearer ' + cfg.anonKey, + 'Prefer': 'return=representation' + }, + body: JSON.stringify(bodyToSend) + }); + const respText = await res.text(); + console.log('Supabase response status', res.status, 'body:', respText); + if(!res.ok){ + throw new Error('Error al enviar: ' + res.status + ' ' + respText); + } + try{ return JSON.parse(respText); }catch(e){ return respText; } } - return res.json(); + + throw new Error('No hay un endpoint seguro ni credenciales de cliente configuradas'); } function serializeForm(form){ const data = {}; const fm = new FormData(form); for(const [k,v] of fm.entries()){ - // Normalize checkbox values + // Normalize checkbox values and booleans + try{ + const el = form.elements[k]; + if(el && el.type === 'checkbox'){ + data[k] = (v === '1' || v === 'on'); + continue; + } + }catch(e){} if(typeof v === 'string'){ data[k] = v; } } + // Remove fields that are not in the registrations table (avoid DB errors) + if(data.page) delete data.page; return data; } diff --git a/2026/form/registro.html b/2026/form/registro.html index 34a1dcf..2172327 100644 --- a/2026/form/registro.html +++ b/2026/form/registro.html @@ -46,7 +46,6 @@

Registro de asistentes

Regístrate para asistir al evento.

- @@ -60,7 +59,7 @@

Registro de asistentes

- +
@@ -85,7 +84,7 @@

Registro de asistentes

- + \ No newline at end of file diff --git a/2026_dev/.gitignore b/2026_dev/.gitignore index ee219a6..c7350a3 100644 --- a/2026_dev/.gitignore +++ b/2026_dev/.gitignore @@ -5,3 +5,7 @@ __pycache__/ .states *.py[cod] .venv +# local form config +pycon_panama_web/**/form/config.js +pycon_panama_web/**/public/src/.env +local_server/.env \ No newline at end of file diff --git a/2026_dev/public/form/config.example.js b/2026_dev/public/form/config.example.js index ea5c75a..4c0cdfb 100644 --- a/2026_dev/public/form/config.example.js +++ b/2026_dev/public/form/config.example.js @@ -1,5 +1,11 @@ // Copia este archivo a config.js y completa los valores reales antes de desplegar. +// Para pruebas locales con el proxy, usa la propiedad `secureEndpoint`. +// No metas la `SERVICE_ROLE_KEY` en este archivo: solo en el proxy (.env). window.SUPABASE_CONFIG = { - url: 'https://your-project.supabase.co', - anonKey: 'YOUR_ANON_KEY' + // Supabase (opcional si usas proxy local) + url: 'https://wfiyucykjoohdiazlqbz.supabase.co', + anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB', + + // Ejemplo de endpoint local que usa el proxy `local_server` + // secureEndpoint: 'http://localhost:9000/submit' }; diff --git a/2026_dev/public/form/config.js b/2026_dev/public/form/config.js new file mode 100644 index 0000000..024987d --- /dev/null +++ b/2026_dev/public/form/config.js @@ -0,0 +1,6 @@ +// Archivo local con configuración de Supabase para formularios (dev). +// NO subir este archivo al repositorio. Está listado en .gitignore. +window.SUPABASE_CONFIG = { + url: 'https://wfiyucykjoohdiazlqbz.supabase.co', + anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' +}; diff --git a/2026_dev/public/form/js/form-submit.js b/2026_dev/public/form/js/form-submit.js index 06d4868..3047b3b 100644 --- a/2026_dev/public/form/js/form-submit.js +++ b/2026_dev/public/form/js/form-submit.js @@ -5,36 +5,64 @@ async function submitToSupabase(table, payload){ const cfg = getConfig(); - if(!cfg || !cfg.url || !cfg.anonKey){ - throw new Error('Falta configuración de Supabase. Crea /form/config.js con las credenciales.'); + const secureEndpoint = cfg && cfg.secureEndpoint; + // If secureEndpoint configured, try server-side proxy first + if (secureEndpoint){ + try{ + const r = await fetch(secureEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ table, payload }) + }); + const text = await r.text(); + if(!r.ok) throw new Error('Server error: ' + r.status + ' ' + text); + try{ return JSON.parse(text); }catch(e){ return text; } + }catch(err){ + console.warn('Secure endpoint failed, falling back to client-side if configured', err); + } } - const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'apikey': cfg.anonKey, - 'Authorization': 'Bearer ' + cfg.anonKey, - 'Prefer': 'return=representation' - }, - body: JSON.stringify(payload) - }); - if(!res.ok){ - const text = await res.text(); - throw new Error('Error al enviar: ' + res.status + ' ' + text); + + // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) + if(cfg && cfg.url && cfg.anonKey){ + const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': cfg.anonKey, + 'Authorization': 'Bearer ' + cfg.anonKey, + 'Prefer': 'return=representation' + }, + body: JSON.stringify(payload) + }); + if(!res.ok){ + const text = await res.text(); + throw new Error('Error al enviar: ' + res.status + ' ' + text); + } + return res.json(); } - return res.json(); + + throw new Error('No hay un endpoint seguro ni credenciales de cliente configuradas'); } function serializeForm(form){ const data = {}; const fm = new FormData(form); for(const [k,v] of fm.entries()){ - // Normalize checkbox values + // Normalize checkbox values and booleans + try{ + const el = form.elements[k]; + if(el && el.type === 'checkbox'){ + data[k] = (v === '1' || v === 'on'); + continue; + } + }catch(e){} if(typeof v === 'string'){ data[k] = v; } } + // Remove fields not in the DB schema + if(data.page) delete data.page; return data; } diff --git a/2026_dev/public/form/registro.html b/2026_dev/public/form/registro.html index 4c3f2b0..7b97153 100644 --- a/2026_dev/public/form/registro.html +++ b/2026_dev/public/form/registro.html @@ -26,7 +26,7 @@

Registro de asistentes

@@ -36,7 +36,7 @@

Registro de asistentes

- - + + \ No newline at end of file From 4fa4e483839296aec0a0e9a460c4ef71ca880364 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Thu, 20 Aug 2026 21:02:23 -0500 Subject: [PATCH 3/9] cambios SUPABASE --- 2026/form/config.example.js | 6 +++--- 2026/form/js/form-submit.js | 16 +++++++++++++--- 2026_dev/public/form/js/form-submit.js | 21 +++++++++++++++++---- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/2026/form/config.example.js b/2026/form/config.example.js index ef33551..eed0195 100644 --- a/2026/form/config.example.js +++ b/2026/form/config.example.js @@ -1,5 +1,5 @@ // Copia este archivo a config.js y completa los valores reales antes de desplegar. window.SUPABASE_CONFIG = { - url: '--', - anonKey: '--' -}; + url: 'https://wfiyucykjoohdiazlqbz.supabase.co', + anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' +}; \ No newline at end of file diff --git a/2026/form/js/form-submit.js b/2026/form/js/form-submit.js index c376104..f05f015 100644 --- a/2026/form/js/form-submit.js +++ b/2026/form/js/form-submit.js @@ -24,13 +24,23 @@ // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) if(cfg && cfg.url && cfg.anonKey){ - let url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + let url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; // Append apikey as query param as a fallback if headers are stripped by CORS/proxy if (cfg.anonKey && url.indexOf('apikey=') === -1){ url += (url.indexOf('?') === -1 ? '?' : '&') + 'apikey=' + encodeURIComponent(cfg.anonKey); } - const bodyToSend = Array.isArray(payload) ? payload : [payload]; - console.log('Direct Supabase POST payload (array):', bodyToSend); + // Build body as array for Supabase + const bodyToSend = Array.isArray(payload) ? payload : [payload]; + // Normalize speaker consent field to match DB column + if(table === 'speakers'){ + bodyToSend.forEach(item => { + if(item.consent !== undefined && item.consent_publication === undefined){ + item.consent_publication = item.consent; + delete item.consent; + } + }); + } + console.log('Direct Supabase POST table:', table, 'payload (array):', bodyToSend); const res = await fetch(url, { method: 'POST', headers: { diff --git a/2026_dev/public/form/js/form-submit.js b/2026_dev/public/form/js/form-submit.js index 3047b3b..89b4101 100644 --- a/2026_dev/public/form/js/form-submit.js +++ b/2026_dev/public/form/js/form-submit.js @@ -25,6 +25,18 @@ // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) if(cfg && cfg.url && cfg.anonKey){ const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; + // Build body as array for Supabase + const bodyToSend = Array.isArray(payload) ? payload : [payload]; + // Normalize speaker consent field to match DB column + if(table === 'speakers'){ + bodyToSend.forEach(item => { + if(item.consent !== undefined && item.consent_publication === undefined){ + item.consent_publication = item.consent; + delete item.consent; + } + }); + } + console.log('Direct Supabase POST table:', table, 'payload (array):', bodyToSend); const res = await fetch(url, { method: 'POST', headers: { @@ -33,13 +45,14 @@ 'Authorization': 'Bearer ' + cfg.anonKey, 'Prefer': 'return=representation' }, - body: JSON.stringify(payload) + body: JSON.stringify(bodyToSend) }); + const respText = await res.text(); + console.log('Supabase response status', res.status, 'body:', respText); if(!res.ok){ - const text = await res.text(); - throw new Error('Error al enviar: ' + res.status + ' ' + text); + throw new Error('Error al enviar: ' + res.status + ' ' + respText); } - return res.json(); + try{ return JSON.parse(respText); }catch(e){ return respText; } } throw new Error('No hay un endpoint seguro ni credenciales de cliente configuradas'); From e1704d77fc1db8f03b544215abb18563f911a374 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 13:35:59 -0500 Subject: [PATCH 4/9] fix(forms): clarify personal data consent --- 2026/form/registro.html | 2 +- 2026/form/speaker.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/2026/form/registro.html b/2026/form/registro.html index 2172327..33b77f2 100644 --- a/2026/form/registro.html +++ b/2026/form/registro.html @@ -60,7 +60,7 @@

Registro de asistentes

- +
diff --git a/2026/form/speaker.html b/2026/form/speaker.html index dfd1c5f..480e449 100644 --- a/2026/form/speaker.html +++ b/2026/form/speaker.html @@ -63,7 +63,7 @@

Proponer ponencia

- +
From b79c3584b042f425659abd07e93d6d37dd93abf3 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 20:20:16 -0500 Subject: [PATCH 5/9] SEO 2026: metadatos y rutas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Actualiza sitemap y validaciones. Usa provisionalmente la mascota del año pasado en las imágenes sociales. --- 2026/about.html | 2 + 2026/agenda.html | 2 + 2026/codigo_conducta.html | 2 + 2026/faq.html | 2 + 2026/form/index.html | 334 -------------------------------------- 2026/form/speaker.html | 331 ------------------------------------- 2026/index.html | 2 + 2026/registro.html | 4 + 2026/sedes.html | 2 + 2026/speaker.html | 8 + netlify.toml | 10 ++ scripts/validate_site.py | 27 ++- sitemap.xml | 26 +-- 13 files changed, 66 insertions(+), 686 deletions(-) delete mode 100644 2026/form/index.html delete mode 100644 2026/form/speaker.html diff --git a/2026/about.html b/2026/about.html index 07b1667..5859554 100644 --- a/2026/about.html +++ b/2026/about.html @@ -8,12 +8,14 @@ + + diff --git a/2026/agenda.html b/2026/agenda.html index a9433fd..8c1f057 100644 --- a/2026/agenda.html +++ b/2026/agenda.html @@ -8,12 +8,14 @@ + + diff --git a/2026/codigo_conducta.html b/2026/codigo_conducta.html index 7c358c1..bb0e8f1 100644 --- a/2026/codigo_conducta.html +++ b/2026/codigo_conducta.html @@ -8,12 +8,14 @@ + + diff --git a/2026/faq.html b/2026/faq.html index 5a05937..46b27ff 100644 --- a/2026/faq.html +++ b/2026/faq.html @@ -8,12 +8,14 @@ + + diff --git a/2026/form/index.html b/2026/form/index.html deleted file mode 100644 index f1c9817..0000000 --- a/2026/form/index.html +++ /dev/null @@ -1,334 +0,0 @@ - - - - - - - Registro - PyCon Panamá 2026 - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
- - -
- -
- -
- - -
-
- -
- - -
- - - - -

-
-
-
-
-
- - - - - - - - - - - - diff --git a/2026/form/speaker.html b/2026/form/speaker.html deleted file mode 100644 index 129b897..0000000 --- a/2026/form/speaker.html +++ /dev/null @@ -1,331 +0,0 @@ - - - - - - - Registro de Speaker - PyCon Panamá 2026 - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
-
- - -
-
- -
- - -
- - -

-
-
-
-
-
- - - - - - - - - - - - diff --git a/2026/index.html b/2026/index.html index 3348278..fd0d6a5 100644 --- a/2026/index.html +++ b/2026/index.html @@ -8,12 +8,14 @@ + + diff --git a/2026/registro.html b/2026/registro.html index ebc64c6..eb76270 100644 --- a/2026/registro.html +++ b/2026/registro.html @@ -8,12 +8,14 @@ + + @@ -111,6 +113,8 @@ } .checkbox-item input { + width: auto; + padding: 0; margin-top: 0.2rem; } diff --git a/2026/sedes.html b/2026/sedes.html index 38b56f8..4bb62e3 100644 --- a/2026/sedes.html +++ b/2026/sedes.html @@ -8,12 +8,14 @@ + + diff --git a/2026/speaker.html b/2026/speaker.html index c3eee89..77af40c 100644 --- a/2026/speaker.html +++ b/2026/speaker.html @@ -8,12 +8,14 @@ + + @@ -91,6 +93,12 @@ resize: vertical; } + .checkbox-item input { + width: auto; + padding: 0; + margin-top: 0.2rem; + } + .field input:focus, .field select:focus, .field textarea:focus { diff --git a/netlify.toml b/netlify.toml index b9f2249..c37f9ae 100644 --- a/netlify.toml +++ b/netlify.toml @@ -10,6 +10,16 @@ from = "/" to = "/2026/" status = 301 +[[redirects]] +from = "/2026/form/index.html" +to = "/2026/registro.html" +status = 301 + +[[redirects]] +from = "/2026/form/speaker.html" +to = "/2026/speaker.html" +status = 301 + [[headers]] for = "/*" [headers.values] diff --git a/scripts/validate_site.py b/scripts/validate_site.py index b4d9a8d..6585dc0 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -14,7 +14,15 @@ EDITION = ROOT / "2026" CANONICAL_BASE = "https://pycon.pa" OPTIONAL_LOCAL_ASSETS = {EDITION / "js" / "env.js"} -REQUIRED_META = {"description", "og:title", "og:description", "og:url", "twitter:card"} +REQUIRED_META = { + "description", + "og:title", + "og:description", + "og:url", + "og:image", + "twitter:card", + "twitter:image", +} SITEMAP_REQUIRED = { f"{CANONICAL_BASE}/2026/", f"{CANONICAL_BASE}/2026/about.html", @@ -110,6 +118,13 @@ def parse_pages() -> tuple[dict[Path, PageParser], list[str]]: errors.append(f"{label}: canonical debe ser {expected}.") if parser.meta.get("og:url") != expected: errors.append(f"{label}: og:url debe ser {expected}.") + for image_key in ("og:image", "twitter:image"): + image = parser.meta.get(image_key, "") + parsed_image = urlsplit(image) + if parsed_image.scheme != "https" or parsed_image.netloc != "pycon.pa": + errors.append(f"{label}: {image_key} debe ser una URL HTTPS absoluta de pycon.pa.") + if len(parser.canonicals) != 1: + errors.append(f"{label}: debe tener exactamente un canonical.") if parser.duplicate_ids: errors.append(f"{label}: IDs duplicados: {', '.join(sorted(parser.duplicate_ids))}.") return pages, errors @@ -143,8 +158,14 @@ def validate_sitemap() -> list[str]: namespace = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"} locations = {node.text for node in tree.findall("sm:url/sm:loc", namespace) if node.text} missing = SITEMAP_REQUIRED - locations - if missing: - return [f"sitemap.xml no incluye: {', '.join(sorted(missing))}."] + extra = locations - SITEMAP_REQUIRED + if missing or extra: + errors = [] + if missing: + errors.append(f"sitemap.xml no incluye: {', '.join(sorted(missing))}.") + if extra: + errors.append(f"sitemap.xml contiene URLs no canónicas: {', '.join(sorted(extra))}.") + return errors return [] diff --git a/sitemap.xml b/sitemap.xml index 34836be..19d6954 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,60 +2,50 @@ https://pycon.pa/2026/ - 2026-08-07 + 2026-08-24 weekly 1.0 https://pycon.pa/2026/about.html - 2026-08-07 + 2026-08-24 monthly 0.8 https://pycon.pa/2026/agenda.html - 2026-08-07 + 2026-08-24 weekly 0.9 https://pycon.pa/2026/sedes.html - 2026-08-22 + 2026-08-24 monthly 0.8 https://pycon.pa/2026/codigo_conducta.html - 2026-08-07 + 2026-08-24 yearly 0.6 https://pycon.pa/2026/faq.html - 2026-08-07 + 2026-08-24 monthly 0.7 https://pycon.pa/2026/registro.html - 2026-08-22 + 2026-08-24 monthly 0.8 https://pycon.pa/2026/speaker.html - 2026-08-22 + 2026-08-24 monthly 0.8 - - https://pycon.pa/2025/ - yearly - 0.3 - - - https://pycon.pa/2024/ - yearly - 0.2 - From 154345012ddf3d2cb7f8845dcdb1ce09e4f2eddd Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 20:30:46 -0500 Subject: [PATCH 6/9] Formularios: alinear checkboxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajusta la simetría de las fechas de asistencia y mantiene la casilla de consentimiento alineada. --- 2026/css/style.css | 6 ++++-- 2026/registro.html | 6 ++++-- 2026/speaker.html | 10 +++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/2026/css/style.css b/2026/css/style.css index b9da7a9..b86bc07 100644 --- a/2026/css/style.css +++ b/2026/css/style.css @@ -806,7 +806,8 @@ } .checkbox-item { - display: flex; + display: grid; + grid-template-columns: 1.1rem minmax(0, 1fr); align-items: flex-start; gap: 0.6rem; color: var(--text-secondary); @@ -814,7 +815,8 @@ } .checkbox-item input { - width: auto; + width: 1.1rem; + height: 1.1rem; margin-top: 0.2rem; } diff --git a/2026/registro.html b/2026/registro.html index eb76270..bb4092e 100644 --- a/2026/registro.html +++ b/2026/registro.html @@ -106,14 +106,16 @@ } .checkbox-item { - display: flex; + display: grid; + grid-template-columns: 1.1rem minmax(0, 1fr); align-items: flex-start; gap: 0.6rem; color: var(--text-secondary); } .checkbox-item input { - width: auto; + width: 1.1rem; + height: 1.1rem; padding: 0; margin-top: 0.2rem; } diff --git a/2026/speaker.html b/2026/speaker.html index 77af40c..becc625 100644 --- a/2026/speaker.html +++ b/2026/speaker.html @@ -94,7 +94,8 @@ } .checkbox-item input { - width: auto; + width: 1.1rem; + height: 1.1rem; padding: 0; margin-top: 0.2rem; } @@ -106,6 +107,13 @@ border-color: var(--accent); } + .checkbox-item { + display: grid; + grid-template-columns: 1.1rem minmax(0, 1fr); + align-items: flex-start; + gap: 0.6rem; + color: var(--text-secondary); + } .status-message { min-height: 1.5rem; margin-top: 0.25rem; From b5caa012bf07ed527a53ac8efa18430d626df830 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 20:39:15 -0500 Subject: [PATCH 7/9] Eliminar formularios duplicados --- 2026/form/README.md | 9 - 2026/form/config.example.js | 5 - 2026/form/config.js | 6 - 2026/form/index.html | 762 ------------------------------------ 2026/form/js/form-submit.js | 113 ------ 2026/form/registro.html | 90 ----- 2026/form/speaker.html | 93 ----- 7 files changed, 1078 deletions(-) delete mode 100644 2026/form/README.md delete mode 100644 2026/form/config.example.js delete mode 100644 2026/form/config.js delete mode 100644 2026/form/index.html delete mode 100644 2026/form/js/form-submit.js delete mode 100644 2026/form/registro.html delete mode 100644 2026/form/speaker.html diff --git a/2026/form/README.md b/2026/form/README.md deleted file mode 100644 index a86ffa7..0000000 --- a/2026/form/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Instrucciones rápidas para integrar con Supabase - -1. En Supabase crea un proyecto y añade dos tablas: `speakers` y `registrations` con columnas que coincidan con los campos del formulario (por ejemplo: name, email, affiliation, bio, title, abstract, duration, language, links, consent para speakers; name, email, organization, role, diet, accessibility, phone, consent_photos para registrations). -2. Habilita CORS para tu dominio en Supabase. -3. Copia `config.example.js` a `config.js` y rellena `url` y `anonKey`. -4. Despliega la carpeta `public/` en tu host (Netlify/Vercel/Netlify Functions). Las páginas estarán disponibles en `/form/speaker.html` y `/form/registro.html`. -5. Verifica en Supabase que los inserts funcionan; revisa la tabla y los registros. - -Notas de seguridad: el `anonKey` es la clave pública pensada para operaciones cliente (inserciones públicas). Para acciones sensibles, crea endpoints server-side. diff --git a/2026/form/config.example.js b/2026/form/config.example.js deleted file mode 100644 index eed0195..0000000 --- a/2026/form/config.example.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copia este archivo a config.js y completa los valores reales antes de desplegar. -window.SUPABASE_CONFIG = { - url: 'https://wfiyucykjoohdiazlqbz.supabase.co', - anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' -}; \ No newline at end of file diff --git a/2026/form/config.js b/2026/form/config.js deleted file mode 100644 index 3c7655f..0000000 --- a/2026/form/config.js +++ /dev/null @@ -1,6 +0,0 @@ -// Archivo local con configuración de Supabase para formularios. -// NO subir este archivo al repositorio. Está listado en .gitignore. -window.SUPABASE_CONFIG = { - url: 'https://wfiyucykjoohdiazlqbz.supabase.co', - anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' -}; diff --git a/2026/form/index.html b/2026/form/index.html deleted file mode 100644 index b414adc..0000000 --- a/2026/form/index.html +++ /dev/null @@ -1,762 +0,0 @@ - - - - - - - Registro al evento - - - - - - - - - - - - - - - -
- - -
-
- -
- -
- -
- -
- -
- PyCon - Panamá - 2025 -
- -
- -
- InicioAcerca de. - AgendaSedesSponsors -
-
-
- -
- -
- -
PyCon Panamá 2025
- - -
-
-
- -
- -
- - - - - - - -
- - -
-
- Registro Formulario - -
- -
- -
- -
- -
- -
- - -
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- - -
- -
- -
- -
- - -
- -
- -
- - -
-
- - -
-
- - -
-
- - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/2026/form/js/form-submit.js b/2026/form/js/form-submit.js deleted file mode 100644 index f05f015..0000000 --- a/2026/form/js/form-submit.js +++ /dev/null @@ -1,113 +0,0 @@ -(function(){ - function getConfig(){ - return window.SUPABASE_CONFIG || null; - } - - async function submitToSupabase(table, payload){ - const cfg = getConfig(); - const secureEndpoint = cfg && cfg.secureEndpoint; - // If secureEndpoint configured, try server-side proxy first - if (secureEndpoint){ - try{ - const r = await fetch(secureEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ table, payload }) - }); - const text = await r.text(); - if(!r.ok) throw new Error('Server error: ' + r.status + ' ' + text); - try{ return JSON.parse(text); }catch(e){ return text; } - }catch(err){ - console.warn('Secure endpoint failed, falling back to client-side if configured', err); - } - } - - // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) - if(cfg && cfg.url && cfg.anonKey){ - let url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; - // Append apikey as query param as a fallback if headers are stripped by CORS/proxy - if (cfg.anonKey && url.indexOf('apikey=') === -1){ - url += (url.indexOf('?') === -1 ? '?' : '&') + 'apikey=' + encodeURIComponent(cfg.anonKey); - } - // Build body as array for Supabase - const bodyToSend = Array.isArray(payload) ? payload : [payload]; - // Normalize speaker consent field to match DB column - if(table === 'speakers'){ - bodyToSend.forEach(item => { - if(item.consent !== undefined && item.consent_publication === undefined){ - item.consent_publication = item.consent; - delete item.consent; - } - }); - } - console.log('Direct Supabase POST table:', table, 'payload (array):', bodyToSend); - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'apikey': cfg.anonKey, - 'Authorization': 'Bearer ' + cfg.anonKey, - 'Prefer': 'return=representation' - }, - body: JSON.stringify(bodyToSend) - }); - const respText = await res.text(); - console.log('Supabase response status', res.status, 'body:', respText); - if(!res.ok){ - throw new Error('Error al enviar: ' + res.status + ' ' + respText); - } - try{ return JSON.parse(respText); }catch(e){ return respText; } - } - - throw new Error('No hay un endpoint seguro ni credenciales de cliente configuradas'); - } - - function serializeForm(form){ - const data = {}; - const fm = new FormData(form); - for(const [k,v] of fm.entries()){ - // Normalize checkbox values and booleans - try{ - const el = form.elements[k]; - if(el && el.type === 'checkbox'){ - data[k] = (v === '1' || v === 'on'); - continue; - } - }catch(e){} - if(typeof v === 'string'){ - data[k] = v; - } - } - // Remove fields that are not in the registrations table (avoid DB errors) - if(data.page) delete data.page; - return data; - } - - function attach(formId){ - const form = document.getElementById(formId); - if(!form) return; - const message = form.querySelector('#form-message'); - form.addEventListener('submit', async (ev) =>{ - ev.preventDefault(); - message.className = 'alert'; - message.textContent = 'Enviando...'; - const table = form.dataset.table; - const payload = serializeForm(form); - try{ - await submitToSupabase(table, payload); - message.className = 'alert alert-success'; - message.textContent = 'Enviado correctamente. Gracias.'; - form.reset(); - }catch(e){ - console.error(e); - message.className = 'alert alert-danger'; - message.textContent = 'Error enviando formulario. Ver consola.'; - } - }); - } - - document.addEventListener('DOMContentLoaded', () =>{ - attach('speaker-form'); - attach('registro-form'); - }); -})(); \ No newline at end of file diff --git a/2026/form/registro.html b/2026/form/registro.html deleted file mode 100644 index 33b77f2..0000000 --- a/2026/form/registro.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - -
-

Registro de asistentes

-

Regístrate para asistir al evento.

-
-
- - - - - - -
- - -
-
- -
-
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/2026/form/speaker.html b/2026/form/speaker.html deleted file mode 100644 index 480e449..0000000 --- a/2026/form/speaker.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - Proponer charla - PyCon Panamá 2026 - - - - - - - - - - - - - -
-

Proponer ponencia

-

Completa el formulario para enviar tu propuesta de charla.

-
-
- - - - - - - - - -
- - -
-
- -
-
-
-
-
- - - - - - - - \ No newline at end of file From 1f99f06407cbd8e27e814a59f1126c2a85124194 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 20:47:52 -0500 Subject: [PATCH 8/9] Excluir formularios dev del PR No incluye configuraciones ni formularios de desarrollo ajenos al issue SEO. --- 2026_dev/public/form/README.md | 9 --- 2026_dev/public/form/config.example.js | 11 --- 2026_dev/public/form/config.js | 6 -- 2026_dev/public/form/js/form-submit.js | 106 ------------------------- 2026_dev/public/form/registro.html | 42 ---------- 2026_dev/public/form/speaker.html | 49 ------------ 6 files changed, 223 deletions(-) delete mode 100644 2026_dev/public/form/README.md delete mode 100644 2026_dev/public/form/config.example.js delete mode 100644 2026_dev/public/form/config.js delete mode 100644 2026_dev/public/form/js/form-submit.js delete mode 100644 2026_dev/public/form/registro.html delete mode 100644 2026_dev/public/form/speaker.html diff --git a/2026_dev/public/form/README.md b/2026_dev/public/form/README.md deleted file mode 100644 index a86ffa7..0000000 --- a/2026_dev/public/form/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Instrucciones rápidas para integrar con Supabase - -1. En Supabase crea un proyecto y añade dos tablas: `speakers` y `registrations` con columnas que coincidan con los campos del formulario (por ejemplo: name, email, affiliation, bio, title, abstract, duration, language, links, consent para speakers; name, email, organization, role, diet, accessibility, phone, consent_photos para registrations). -2. Habilita CORS para tu dominio en Supabase. -3. Copia `config.example.js` a `config.js` y rellena `url` y `anonKey`. -4. Despliega la carpeta `public/` en tu host (Netlify/Vercel/Netlify Functions). Las páginas estarán disponibles en `/form/speaker.html` y `/form/registro.html`. -5. Verifica en Supabase que los inserts funcionan; revisa la tabla y los registros. - -Notas de seguridad: el `anonKey` es la clave pública pensada para operaciones cliente (inserciones públicas). Para acciones sensibles, crea endpoints server-side. diff --git a/2026_dev/public/form/config.example.js b/2026_dev/public/form/config.example.js deleted file mode 100644 index 4c0cdfb..0000000 --- a/2026_dev/public/form/config.example.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copia este archivo a config.js y completa los valores reales antes de desplegar. -// Para pruebas locales con el proxy, usa la propiedad `secureEndpoint`. -// No metas la `SERVICE_ROLE_KEY` en este archivo: solo en el proxy (.env). -window.SUPABASE_CONFIG = { - // Supabase (opcional si usas proxy local) - url: 'https://wfiyucykjoohdiazlqbz.supabase.co', - anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB', - - // Ejemplo de endpoint local que usa el proxy `local_server` - // secureEndpoint: 'http://localhost:9000/submit' -}; diff --git a/2026_dev/public/form/config.js b/2026_dev/public/form/config.js deleted file mode 100644 index 024987d..0000000 --- a/2026_dev/public/form/config.js +++ /dev/null @@ -1,6 +0,0 @@ -// Archivo local con configuración de Supabase para formularios (dev). -// NO subir este archivo al repositorio. Está listado en .gitignore. -window.SUPABASE_CONFIG = { - url: 'https://wfiyucykjoohdiazlqbz.supabase.co', - anonKey: 'sb_publishable_wlIN6gMmG_pVr-h-MAaLOw_jUyW4pmB' -}; diff --git a/2026_dev/public/form/js/form-submit.js b/2026_dev/public/form/js/form-submit.js deleted file mode 100644 index 89b4101..0000000 --- a/2026_dev/public/form/js/form-submit.js +++ /dev/null @@ -1,106 +0,0 @@ -(function(){ - function getConfig(){ - return window.SUPABASE_CONFIG || null; - } - - async function submitToSupabase(table, payload){ - const cfg = getConfig(); - const secureEndpoint = cfg && cfg.secureEndpoint; - // If secureEndpoint configured, try server-side proxy first - if (secureEndpoint){ - try{ - const r = await fetch(secureEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ table, payload }) - }); - const text = await r.text(); - if(!r.ok) throw new Error('Server error: ' + r.status + ' ' + text); - try{ return JSON.parse(text); }catch(e){ return text; } - }catch(err){ - console.warn('Secure endpoint failed, falling back to client-side if configured', err); - } - } - - // Fallback to direct client-side POST using anonKey (requires RLS allowing inserts) - if(cfg && cfg.url && cfg.anonKey){ - const url = cfg.url.replace(/\/$/,'') + '/rest/v1/' + table; - // Build body as array for Supabase - const bodyToSend = Array.isArray(payload) ? payload : [payload]; - // Normalize speaker consent field to match DB column - if(table === 'speakers'){ - bodyToSend.forEach(item => { - if(item.consent !== undefined && item.consent_publication === undefined){ - item.consent_publication = item.consent; - delete item.consent; - } - }); - } - console.log('Direct Supabase POST table:', table, 'payload (array):', bodyToSend); - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'apikey': cfg.anonKey, - 'Authorization': 'Bearer ' + cfg.anonKey, - 'Prefer': 'return=representation' - }, - body: JSON.stringify(bodyToSend) - }); - const respText = await res.text(); - console.log('Supabase response status', res.status, 'body:', respText); - if(!res.ok){ - throw new Error('Error al enviar: ' + res.status + ' ' + respText); - } - try{ return JSON.parse(respText); }catch(e){ return respText; } - } - - throw new Error('No hay un endpoint seguro ni credenciales de cliente configuradas'); - } - - function serializeForm(form){ - const data = {}; - const fm = new FormData(form); - for(const [k,v] of fm.entries()){ - // Normalize checkbox values and booleans - try{ - const el = form.elements[k]; - if(el && el.type === 'checkbox'){ - data[k] = (v === '1' || v === 'on'); - continue; - } - }catch(e){} - if(typeof v === 'string'){ - data[k] = v; - } - } - // Remove fields not in the DB schema - if(data.page) delete data.page; - return data; - } - - function attach(formId){ - const form = document.getElementById(formId); - if(!form) return; - const message = form.querySelector('#form-message'); - form.addEventListener('submit', async (ev) =>{ - ev.preventDefault(); - message.textContent = 'Enviando...'; - const table = form.dataset.table; - const payload = serializeForm(form); - try{ - await submitToSupabase(table, payload); - message.textContent = 'Enviado correctamente. Gracias.'; - form.reset(); - }catch(e){ - console.error(e); - message.textContent = 'Error enviando formulario. Ver consola.'; - } - }); - } - - document.addEventListener('DOMContentLoaded', () =>{ - attach('speaker-form'); - attach('registro-form'); - }); -})(); \ No newline at end of file diff --git a/2026_dev/public/form/registro.html b/2026_dev/public/form/registro.html deleted file mode 100644 index 7b97153..0000000 --- a/2026_dev/public/form/registro.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - Registro - PyCon Panamá 2026 - - - - -
-

Registro de asistentes

-

Regístrate para asistir al evento.

-
- - - - - - - - -
- -
-
-
-
- - - - - \ No newline at end of file diff --git a/2026_dev/public/form/speaker.html b/2026_dev/public/form/speaker.html deleted file mode 100644 index 15c4e66..0000000 --- a/2026_dev/public/form/speaker.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - Proponer charla - PyCon Panamá 2026 - - - - -
-

Proponer ponencia

-

Completa el formulario para enviar tu propuesta de charla.

-
- - - - - - - - - - -
- -
-
-
-
- - - - - \ No newline at end of file From 5c370b0f9f27f714bd83ef48ef22cf98eb9738a3 Mon Sep 17 00:00:00 2001 From: Gino Portacio Date: Mon, 24 Aug 2026 20:56:22 -0500 Subject: [PATCH 9/9] Quitar cambios ajenos del PR --- 2026_dev/.gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/2026_dev/.gitignore b/2026_dev/.gitignore index c7350a3..ee219a6 100644 --- a/2026_dev/.gitignore +++ b/2026_dev/.gitignore @@ -5,7 +5,3 @@ __pycache__/ .states *.py[cod] .venv -# local form config -pycon_panama_web/**/form/config.js -pycon_panama_web/**/public/src/.env -local_server/.env \ No newline at end of file