idk
This commit is contained in:
1 parent
e68d357177
commit
3e3382df57
65 files changed
+1836
-42
No files matched your search
@@ -64,6 +64,14 @@ SSO_REQUIRED=false
|
|||||||
# Notificações → Chaves de app.
|
# Notificações → Chaves de app.
|
||||||
NOTIFY_API_KEY=
|
NOTIFY_API_KEY=
|
||||||
|
|
||||||
|
# ─── Hub social content endpoint (api/hub-content.php) ─────────────────────────
|
||||||
|
# Shared secret the hub social-post generator signs its pull requests with.
|
||||||
|
# Generated on id.andmore.pt → Plataformas → fitme3070 → "Gerar segredo de
|
||||||
|
# conteúdo", and registered there as this platform's content_secret with the
|
||||||
|
# content_endpoint pointing at {APP_BASE_URL}api/hub-content.php. Empty disables
|
||||||
|
# the endpoint (503).
|
||||||
|
HUB_CONTENT_SECRET=
|
||||||
|
|
||||||
# ─── Git HTTP authentication (views/sysadmin/platform_update.php) ──────────────
|
# ─── Git HTTP authentication (views/sysadmin/platform_update.php) ──────────────
|
||||||
GIT_HTTP_USERNAME=
|
GIT_HTTP_USERNAME=
|
||||||
GIT_HTTP_TOKEN=
|
GIT_HTTP_TOKEN=
|
||||||
@@ -12,6 +12,9 @@ config/cache/*.php
|
|||||||
uploads/print/*
|
uploads/print/*
|
||||||
!uploads/print/.gitkeep
|
!uploads/print/.gitkeep
|
||||||
|
|
||||||
|
uploads/events/*
|
||||||
|
!uploads/events/.gitkeep
|
||||||
|
|
||||||
uploads/sessions/*
|
uploads/sessions/*
|
||||||
!uploads/sessions/.gitkeep
|
!uploads/sessions/.gitkeep
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ RewriteRule ^home/contactos/?$ site/contactos.php [L,QSA]
|
|||||||
RewriteRule ^home/agendar/?$ site/agendar.php [L,QSA]
|
RewriteRule ^home/agendar/?$ site/agendar.php [L,QSA]
|
||||||
RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA]
|
RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA]
|
||||||
RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA]
|
RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA]
|
||||||
|
RewriteRule ^evento/([a-zA-Z0-9-]+)/?$ site/evento.php?slug=$1 [L,QSA]
|
||||||
|
|
||||||
# Pass the Authorization header through to PHP
|
# Pass the Authorization header through to PHP
|
||||||
RewriteCond %{HTTP:Authorization} .
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
// Content endpoint for the id.andmore.pt / hub social-post generator (hub pull).
|
||||||
|
//
|
||||||
|
// Server-to-server, no session: the hub signs each request with the shared
|
||||||
|
// HUB_CONTENT_SECRET (.env; generated on id.andmore.pt → Plataformas →
|
||||||
|
// fitme3070 → "Gerar segredo de conteúdo"):
|
||||||
|
// X-Hub-Date: ISO-8601 timestamp (±10 min tolerance)
|
||||||
|
// X-Hub-Token: hash_hmac('sha256', date . path, secret)
|
||||||
|
//
|
||||||
|
// Response is the generic hub envelope. One item type:
|
||||||
|
// event — the weekly class schedule (dias da semana + as aulas nesses dias).
|
||||||
|
// Each recurring class is emitted as an event whose date_start is the
|
||||||
|
// next occurrence of that weekday/time, so the hub renders the day
|
||||||
|
// name automatically; the weekday is also given explicitly in fields.
|
||||||
|
//
|
||||||
|
// GET params: from=Y-m-d to=Y-m-d type=event (all optional; the schedule is
|
||||||
|
// recurring, so the date range is not used to filter).
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/db.php';
|
||||||
|
require_once __DIR__ . '/../config/functions.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$secret = (string) env('HUB_CONTENT_SECRET', '');
|
||||||
|
if ($secret === '') {
|
||||||
|
http_response_code(503);
|
||||||
|
echo json_encode(['error' => 'HUB_CONTENT_SECRET não configurado nesta plataforma.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = (string) ($_SERVER['HTTP_X_HUB_DATE'] ?? '');
|
||||||
|
$token = (string) ($_SERVER['HTTP_X_HUB_TOKEN'] ?? '');
|
||||||
|
$path = (string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '');
|
||||||
|
$ts = $date !== '' ? strtotime($date) : false;
|
||||||
|
|
||||||
|
if ($token === '' || $ts === false || abs(time() - $ts) > 600
|
||||||
|
|| !hash_equals(hash_hmac('sha256', $date . $path, $secret), $token)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Assinatura inválida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
session_write_close(); // not needed past auth; release the session lock
|
||||||
|
|
||||||
|
$type = (string) ($_GET['type'] ?? '');
|
||||||
|
|
||||||
|
// Optional category filter: ?category=segunda or ?categories=segunda,quarta.
|
||||||
|
// For this platform the category dimension is the weekday. Empty = all.
|
||||||
|
$cat_filter = array_filter(array_map(
|
||||||
|
fn($c) => strtolower(trim($c)),
|
||||||
|
preg_split('/[,]/', (string) ($_GET['categories'] ?? $_GET['category'] ?? ''))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Weekday enum → display label and ISO day-of-week (1=Mon … 7=Sun).
|
||||||
|
$WEEKDAYS = [
|
||||||
|
'segunda' => ['Segunda-feira', 1],
|
||||||
|
'terça' => ['Terça-feira', 2],
|
||||||
|
'quarta' => ['Quarta-feira', 3],
|
||||||
|
'quinta' => ['Quinta-feira', 4],
|
||||||
|
'sexta' => ['Sexta-feira', 5],
|
||||||
|
'sábado' => ['Sábado', 6],
|
||||||
|
'domingo' => ['Domingo', 7],
|
||||||
|
];
|
||||||
|
$LOCALS = ['sala1' => 'Sala 1', 'sala2' => 'Sala 2', 'crossfit' => 'CrossFit'];
|
||||||
|
|
||||||
|
// Next occurrence (today or in the coming week) of a given weekday at $time.
|
||||||
|
$next_occurrence = static function (int $isoDow, string $time): DateTime {
|
||||||
|
$dt = new DateTime('today');
|
||||||
|
$diff = ($isoDow - (int) $dt->format('N') + 7) % 7; // 0 = today
|
||||||
|
if ($diff > 0) $dt->modify("+{$diff} day");
|
||||||
|
[$h, $m, $s] = array_pad(explode(':', $time), 3, '0');
|
||||||
|
$dt->setTime((int) $h, (int) $m, (int) $s);
|
||||||
|
return $dt;
|
||||||
|
};
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
// ── event: weekly class schedule ────────────────────────────────────────────
|
||||||
|
if ($type === '' || $type === 'event') {
|
||||||
|
// db_horario_list() already joins modalidades and orders segunda→domingo,
|
||||||
|
// then by start time; restrict to live modalidades (not soft-deleted).
|
||||||
|
$rows = $pdo->query("
|
||||||
|
SELECT h.id, h.dia_semana, h.local,
|
||||||
|
m.nome AS modalidade_nome,
|
||||||
|
TIME_FORMAT(h.hora_inicio, '%H:%i') AS hora_inicio_fmt,
|
||||||
|
TIME_FORMAT(h.hora_fim, '%H:%i') AS hora_fim_fmt
|
||||||
|
FROM horario h
|
||||||
|
JOIN modalidades m ON m.m_id = h.id_modalidade AND m.deleted_at IS NULL
|
||||||
|
ORDER BY FIELD(h.dia_semana,'segunda','terça','quarta','quinta','sexta','sábado','domingo'),
|
||||||
|
h.hora_inicio ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$dia = (string) $r['dia_semana'];
|
||||||
|
if (!isset($WEEKDAYS[$dia])) continue; // unknown / NULL weekday — skip
|
||||||
|
if ($cat_filter && !in_array($dia, $cat_filter, true)) continue; // category filter
|
||||||
|
[$diaLabel, $isoDow] = $WEEKDAYS[$dia];
|
||||||
|
|
||||||
|
$ini = (string) $r['hora_inicio_fmt'];
|
||||||
|
$fim = (string) $r['hora_fim_fmt'];
|
||||||
|
$hora = $fim !== '' ? ($ini . '–' . $fim) : $ini;
|
||||||
|
$local = $LOCALS[(string) $r['local']] ?? '';
|
||||||
|
|
||||||
|
$start = $next_occurrence($isoDow, $ini !== '' ? $ini . ':00' : '00:00:00');
|
||||||
|
$end = $fim !== '' ? $next_occurrence($isoDow, $fim . ':00') : null;
|
||||||
|
|
||||||
|
$fields = ['Dia' => $diaLabel, 'Hora' => $hora];
|
||||||
|
if ($local !== '') $fields['Local'] = $local;
|
||||||
|
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'aula-' . (int) $r['id'],
|
||||||
|
'type' => 'event',
|
||||||
|
'title' => (string) $r['modalidade_nome'],
|
||||||
|
'subtitle' => $hora . ($local !== '' ? ' · ' . $local : ''),
|
||||||
|
'date_start' => $start->format('c'),
|
||||||
|
'date_end' => $end ? $end->format('c') : '',
|
||||||
|
'url' => base_url('home/horario'),
|
||||||
|
'category' => $dia, // machine key (weekday enum value)
|
||||||
|
'category_label' => $diaLabel, // human label
|
||||||
|
'fields' => $fields,
|
||||||
|
'media' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distinct categories present, in item order (segunda→domingo), with counts —
|
||||||
|
// lets the hub offer a category filter without re-deriving it.
|
||||||
|
$categories = [];
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$k = $it['category'];
|
||||||
|
if ($k === '') continue;
|
||||||
|
if (!isset($categories[$k])) {
|
||||||
|
$categories[$k] = ['key' => $k, 'label' => $it['category_label'], 'count' => 0];
|
||||||
|
}
|
||||||
|
$categories[$k]['count']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'app' => 'fitme3070',
|
||||||
|
'generated_at' => gmdate('c'),
|
||||||
|
'item_types' => array_values(array_unique(array_column($items, 'type'))),
|
||||||
|
'category_label'=> 'Dia da semana', // what the category dimension means here
|
||||||
|
'categories' => array_values($categories),
|
||||||
|
'items' => $items,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
|
||||||
|
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}function fe(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}ce.fn=ce.prototype={jquery:t,constructor:ce,length:0,toArray:function(){return ae.call(this)},get:function(e){return null==e?ae.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=ce.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return ce.each(this,e)},map:function(n){return this.pushStack(ce.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(ae.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(ce.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(ce.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:oe.sort,splice:oe.splice},ce.extend=ce.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||v(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(ce.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||ce.isPlainObject(n)?n:{},i=!1,a[t]=ce.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},ce.extend({expando:"jQuery"+(t+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==i.call(e))&&(!(t=r(e))||"function"==typeof(n=ue.call(t,"constructor")&&t.constructor)&&o.call(n)===a)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){m(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(c(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},text:function(e){var t,n="",r=0,i=e.nodeType;if(!i)while(t=e[r++])n+=ce.text(t);return 1===i||11===i?e.textContent:9===i?e.documentElement.textContent:3===i||4===i?e.nodeValue:n},makeArray:function(e,t){var n=t||[];return null!=e&&(c(Object(e))?ce.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:se.call(t,e,n)},isXMLDoc:function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!l.test(t||n&&n.nodeName||"HTML")},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(c(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:le}),"function"==typeof Symbol&&(ce.fn[Symbol.iterator]=oe[Symbol.iterator]),ce.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var pe=oe.pop,de=oe.sort,he=oe.splice,ge="[\\x20\\t\\r\\n\\f]",ve=new RegExp("^"+ge+"+|((?:^|[^\\\\])(?:\\\\.)*)"+ge+"+$","g");ce.contains=function(e,t){var n=t&&t.parentNode;return e===n||!(!n||1!==n.nodeType||!(e.contains?e.contains(n):e.compareDocumentPosition&&16&e.compareDocumentPosition(n)))};var f=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function p(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}ce.escapeSelector=function(e){return(e+"").replace(f,p)};var ye=C,me=s;!function(){var e,b,w,o,a,T,r,C,d,i,k=me,S=ce.expando,E=0,n=0,s=W(),c=W(),u=W(),h=W(),l=function(e,t){return e===t&&(a=!0),0},f="checked|selected|async|autofocus|autoplay|conLine truncated
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
/*! Summernote v0.9.1 | (c) 2013~ Hackerwins and contributors | MIT license */
|
||||||
|
!function(a,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o=e();for(var r in o)("object"==typeof exports?exports:a)[r]=o[r]}}(self,(()=>{return(a=jQuery).extend(!0,a.summernote.lang,{"pt-PT":{font:{bold:"Negrito",italic:"Itálico",underline:"Sublinhado",clear:"Remover estilo da fonte",height:"Altura da linha",name:"Fonte",strikethrough:"Riscado",subscript:"Subscript",superscript:"Superscript",size:"Tamanho da fonte"},image:{image:"Imagem",insert:"Inserir imagem",resizeFull:"Redimensionar Completo",resizeHalf:"Redimensionar Metade",resizeQuarter:"Redimensionar Um Quarto",floatLeft:"Float Esquerda",floatRight:"Float Direita",floatNone:"Sem Float",shapeRounded:"Forma: Arredondado",shapeCircle:"Forma: Círculo",shapeThumbnail:"Forma: Minhatura",shapeNone:"Forma: Nenhum",dragImageHere:"Arraste uma imagem para aqui",dropImage:"Arraste uma imagem ou texto",selectFromFiles:"Selecione a partir dos arquivos",maximumFileSize:"Tamanho máximo do fixeiro",maximumFileSizeError:"Tamanho máximo do fixeiro é maior que o permitido.",url:"Endereço da imagem",remove:"Remover Imagem",original:"Original"},video:{video:"Vídeo",videoLink:"Link para vídeo",insert:"Inserir vídeo",url:"URL do vídeo?",providers:"(YouTube, Google Drive, Vimeo, Vine, Instagram, DailyMotion or Youku)"},link:{link:"Link",insert:"Inserir ligação",unlink:"Remover ligação",edit:"Editar",textToDisplay:"Texto para exibir",url:"Que endereço esta licação leva?",openInNewWindow:"Abrir numa nova janela"},table:{table:"Tabela",addRowAbove:"Adicionar linha acima",addRowBelow:"Adicionar linha abaixo",addColLeft:"Adicionar coluna à Esquerda",addColRight:"Adicionar coluna à Esquerda",delRow:"Excluir linha",delCol:"Excluir coluna",delTable:"Excluir tabela"},hr:{insert:"Inserir linha horizontal"},style:{style:"Estilo",p:"Parágrafo",blockquote:"Citação",pre:"Código",h1:"Título 1",h2:"Título 2",h3:"Título 3",h4:"Título 4",h5:"Título 5",h6:"Título 6"},lists:{unordered:"Lista com marcadores",ordered:"Lista numerada"},options:{help:"Ajuda",fullscreen:"Janela Completa",codeview:"Ver código-fonte"},paragraph:{paragraph:"Parágrafo",outdent:"Menor tabulação",indent:"Maior tabulação",left:"Alinhar à esquerda",center:"Alinhar ao centro",right:"Alinha à direita",justify:"Justificado"},color:{recent:"Cor recente",more:"Mais cores",background:"Fundo",foreground:"Fonte",transparent:"Transparente",setTransparent:"Fundo transparente",reset:"Restaurar",resetToDefault:"Restaurar padrão",cpSelect:"Selecionar"},shortcut:{shortcuts:"Atalhos do teclado",close:"Fechar",textFormatting:"Formatação de texto",action:"Ação",paragraphFormatting:"Formatação de parágrafo",documentStyle:"Estilo de documento"},help:{insertParagraph:"Inserir Parágrafo",undo:"Desfazer o último comando",redo:"Refazer o último comando",tab:"Maior tabulação",untab:"Menor tabulação",bold:"Colocar em negrito",italic:"Colocar em itálico",underline:"Colocar em sublinhado",strikethrough:"Colocar em riscado",removeFormat:"Limpar o estilo",justifyLeft:"Definir alinhado à esquerda",justifyCenter:"Definir alinhado ao centro",justifyRight:"Definir alinhado à direita",justifyFull:"Definir justificado",insertUnorderedList:"Alternar lista não ordenada",insertOrderedList:"Alternar lista ordenada",outdent:"Recuar parágrafo atual",indent:"Avançar parágrafo atual",formatPara:"Alterar formato do bloco para parágrafo",formatH1:"Alterar formato do bloco para Título 1",formatH2:"Alterar formato do bloco para Título 2",formatH3:"Alterar formato do bloco para Título 3",formatH4:"Alterar formato do bloco para Título 4",formatH5:"Alterar formato do bloco para Título 5",formatH6:"Alterar formato do bloco para Título 6",insertHorizontalRule:"Inserir linha horizontal","linkDialog.show":"Inserir uma ligração"},history:{undo:"Desfazer",redo:"Refazer"},specialChar:{specialChar:"SPECIAL CHARACTERS",select:"Select Special characters"}}}),{};var a}));
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@font-face{font-display:auto;font-family:summernote;font-style:normal;font-weight:400;src:url(font/summernote.eot?#iefix) format("embedded-opentype"),url(font/summernote.woff2) format("woff2"),url(font/summernote.woff) format("woff"),url(font/summernote.ttf) format("truetype")}[class*=" note-icon"]:before,[class^=note-icon]:before{display:inline-block;font-family:summernote;font-size:inherit;font-style:normal;text-decoration:inherit;text-rendering:auto;text-transform:none;vertical-align:middle;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;speak:none}.note-icon-fw{text-align:center;width:1.25em}.note-icon-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.note-icon-pull-left{float:left}.note-icon-pull-right{float:right}.note-icon.note-icon-pull-left{margin-right:.3em}.note-icon.note-icon-pull-right{margin-left:.3em}.note-icon-align:before{content:"\ea01"}.note-icon-align-center:before{content:"\ea02"}.note-icon-align-indent:before{content:"\ea03"}.note-icon-align-justify:before{content:"\ea04"}.note-icon-align-left:before{content:"\ea05"}.note-icon-align-outdent:before{content:"\ea06"}.note-icon-align-right:before{content:"\ea07"}.note-icon-arrow-circle-down:before{content:"\ea08"}.note-icon-arrow-circle-left:before{content:"\ea09"}.note-icon-arrow-circle-right:before{content:"\ea0a"}.note-icon-arrow-circle-up:before{content:"\ea0b"}.note-icon-arrows-alt:before{content:"\ea0c"}.note-icon-arrows-h:before{content:"\ea0d"}.note-icon-arrows-v:before{content:"\ea0e"}.note-icon-bold:before{content:"\ea0f"}.note-icon-caret:before{content:"\ea10"}.note-icon-chain-broken:before{content:"\ea11"}.note-icon-circle:before{content:"\ea12"}.note-icon-close:before{content:"\ea13"}.note-icon-code:before{content:"\ea14"}.note-icon-col-after:before{content:"\ea15"}.note-icon-col-before:before{content:"\ea16"}.note-icon-col-remove:before{content:"\ea17"}.note-icon-eraser:before{content:"\ea18"}.note-icon-float-left:before{content:"\ea19"}.note-icon-float-none:before{content:"\ea1a"}.note-icon-float-right:before{content:"\ea1b"}.note-icon-font:before{content:"\ea1c"}.note-icon-frame:before{content:"\ea1d"}.note-icon-italic:before{content:"\ea1e"}.note-icon-link:before{content:"\ea1f"}.note-icon-magic:before{content:"\ea20"}.note-icon-menu-check:before{content:"\ea21"}.note-icon-minus:before{content:"\ea22"}.note-icon-orderedlist:before{content:"\ea23"}.note-icon-pencil:before{content:"\ea24"}.note-icon-picture:before{content:"\ea25"}.note-icon-question:before{content:"\ea26"}.note-icon-redo:before{content:"\ea27"}.note-icon-rollback:before{content:"\ea28"}.note-icon-row-above:before{content:"\ea29"}.note-icon-row-below:before{content:"\ea2a"}.note-icon-row-remove:before{content:"\ea2b"}.note-icon-special-character:before{content:"\ea2c"}.note-icon-square:before{content:"\ea2d"}.note-icon-strikethrough:before{content:"\ea2e"}.note-icon-subscript:before{content:"\ea2f"}.note-icon-summernote:before{content:"\ea30"}.note-icon-superscript:before{content:"\ea31"}.note-icon-table:before{content:"\ea32"}.note-icon-text-height:before{content:"\ea33"}.note-icon-trash:before{content:"\ea34"}.note-icon-underline:before{content:"\ea35"}.note-icon-undo:before{content:"\ea36"}.note-icon-unorderedlist:before{content:"\ea37"}.note-icon-video:before{content:"\ea38"}.note-editor{position:relative}.note-editor .note-dropzone{background-color:#fff;color:#87cefa;display:none;opacity:.95;position:absolute;z-index:100}.note-editor .note-dropzone .note-dropzone-message{display:table-cell;font-size:28px;font-weight:700;text-align:center;vertical-align:middle}.note-editor .note-dropzone.hover{color:#098ddf}.note-editor.dragover .note-dropzone{display:table}.note-editor .note-editing-area{position:relative}.note-editor .note-editing-area .note-editable{outline:none}.note-editor .note-editing-area .note-editable sup{vertical-align:super}.note-editor .note-editing-area .note-editable sub{vertical-align:sub}.note-editor .note-editing-area .note-editable img.note-float-left{margin-right:10px}.note-editor .note-editing-area .note-editable img.note-float-right{margin-left:10px}.note-editor.note-airframe,.note-editor.note-frame{border:1px solid rgba(0,0,0,.196)}.note-editor.note-airframe.codeview .note-editing-area .note-editable,.note-editor.note-frame.codeview .note-editing-area .note-editable{display:none}.note-editor.note-airframe.codeview .note-editing-area .note-codable,.note-editor.note-frame.codeview .note-editing-area .note-codable{display:block}.note-editor.note-airframe .note-editing-area,.note-editor.note-frame .note-editing-area{overflow:hidden}.note-editor.note-airframe .note-editing-area .note-editable,.note-editor.note-frame .note-editing-area .note-editable{overflow:auto;padding:10px;word-wrap:break-word}.note-editor.note-airframe .note-editing-area .note-editable[contenteditable=false],.note-editor.note-frame .note-editing-area .note-editable[contenteditable=false]{background-color:hsla(0,0%,50%,.Line truncated
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! Summernote v0.9.1 | (c) 2013~ Hackerwins and contributors | MIT license */
|
||||||
|
!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("jquery"));else if("function"==typeof define&&define.amd)define(["jquery"],e);else{var o="object"==typeof exports?e(require("jquery")):e(t.jQuery);for(var n in o)("object"==typeof exports?exports:t)[n]=o[n]}}(self,(t=>(()=>{"use strict";var e={7e3:(t,e,o)=>{var n=o(8938),i=o.n(n);i().summernote=i().summernote||{lang:{}},i().extend(!0,i().summernote.lang,{"en-US":{font:{bold:"Bold",italic:"Italic",underline:"Underline",clear:"Remove Font Style",height:"Line Height",name:"Font Family",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript",size:"Font Size",sizeunit:"Font Size Unit"},image:{image:"Picture",insert:"Insert Image",resizeFull:"Resize full",resizeHalf:"Resize half",resizeQuarter:"Resize quarter",resizeNone:"Original size",floatLeft:"Float Left",floatRight:"Float Right",floatNone:"Remove float",shapeRounded:"Shape: Rounded",shapeCircle:"Shape: Circle",shapeThumbnail:"Shape: Thumbnail",shapeNone:"Shape: None",dragImageHere:"Drag image or text here",dropImage:"Drop image or Text",selectFromFiles:"Select from files",maximumFileSize:"Maximum file size",maximumFileSizeError:"Maximum file size exceeded.",url:"Image URL",remove:"Remove Image",original:"Original"},video:{video:"Video",videoLink:"Video Link",insert:"Insert Video",url:"Video URL",providers:"(YouTube, Google Drive, Vimeo, Vine, Instagram, DailyMotion, Youku, Peertube)"},link:{link:"Link",insert:"Insert Link",unlink:"Unlink",edit:"Edit",textToDisplay:"Text to display",url:"To what URL should this link go?",openInNewWindow:"Open in new window"},table:{table:"Table",addRowAbove:"Add row above",addRowBelow:"Add row below",addColLeft:"Add column left",addColRight:"Add column right",delRow:"Delete row",delCol:"Delete column",delTable:"Delete table"},hr:{insert:"Insert Horizontal Rule"},style:{style:"Style",p:"Normal",blockquote:"Quote",pre:"Code",h1:"Header 1",h2:"Header 2",h3:"Header 3",h4:"Header 4",h5:"Header 5",h6:"Header 6"},lists:{unordered:"Unordered list",ordered:"Ordered list"},options:{help:"Help",fullscreen:"Full Screen",codeview:"Code View"},paragraph:{paragraph:"Paragraph",outdent:"Outdent",indent:"Indent",left:"Align left",center:"Align center",right:"Align right",justify:"Justify full"},color:{recent:"Recent Color",more:"More Color",background:"Background Color",foreground:"Text Color",transparent:"Transparent",setTransparent:"Set transparent",reset:"Reset",resetToDefault:"Reset to default",cpSelect:"Select"},shortcut:{shortcuts:"Keyboard shortcuts",close:"Close",textFormatting:"Text formatting",action:"Action",paragraphFormatting:"Paragraph formatting",documentStyle:"Document Style",extraKeys:"Extra keys"},help:{escape:"Escape",insertParagraph:"Insert Paragraph",undo:"Undo the last command",redo:"Redo the last command",tab:"Tab",untab:"Untab",bold:"Set a bold style",italic:"Set a italic style",underline:"Set a underline style",strikethrough:"Set a strikethrough style",removeFormat:"Clean a style",justifyLeft:"Set left align",justifyCenter:"Set center align",justifyRight:"Set right align",justifyFull:"Set full align",insertUnorderedList:"Toggle unordered list",insertOrderedList:"Toggle ordered list",outdent:"Outdent on current paragraph",indent:"Indent on current paragraph",formatPara:"Change current block's format as a paragraph(P tag)",formatH1:"Change current block's format as H1",formatH2:"Change current block's format as H2",formatH3:"Change current block's format as H3",formatH4:"Change current block's format as H4",formatH5:"Change current block's format as H5",formatH6:"Change current block's format as H6",insertHorizontalRule:"Insert horizontal rule","linkDialog.show":"Show Link Dialog"},history:{undo:"Undo",redo:"Redo"},specialChar:{specialChar:"SPECIAL CHARACTERS",select:"Select Special characters"},output:{noSelection:"No Selection Made!"}}})},8938:e=>{e.exports=t}},o={};function n(t){var i=o[t];if(void 0!==i)return i.exports;var r=o[t]={exports:{}};return e[t](r,r.exports,n),r.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var i=n(8938),r=n.n(i),a=(n(7e3),["sans-serif","serif","monospace","cursive","fantasy"]);function s(t){return-1===r().inArray(t.toLowerCase(),a)?"'".concat(t,"'"):t}var l,c=navigator.userAgent,u=/MSIE|Trident/i.test(c);if(u){var d=/MSIE (\d+[.]\d+)/.exec(c);d&&(l=parseFloat(d[1])),(d=/Trident\/.*rv:([0-9]{1,}[.0-9]{0,})/.exec(c))&&(l=parseFloat(d[1]))}var f=/Edge\/\d+/.test(c),h="ontouchstart"in window||navigator.MaxTouchPoints>0||navigator.msMaxTouchPoints>0,p=u?"DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted":"input";const m={isMac:navigator.appVersion.indexOf("Mac")>-1,isMSIE:u,isEdge:f,isFF:!f&&/firefox/i.test(c),isPhantom:/PhantomJS/i.test(c),isWebkit:!f&&/webkit/i.test(c),isChrome:!f&&/Line truncated
|
||||||
+10
-2
@@ -49,8 +49,16 @@ $pass = env('DB_PASS');
|
|||||||
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
|
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO($dsn, $user, $pass);
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
// Real server-side prepares (not driver emulation): a tighter defence
|
||||||
|
// against injection than emulated string-interpolation. Safe here — the
|
||||||
|
// codebase uses positional ? params (no reused :named, no bound LIMIT).
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
// Keep values coming back as strings, as the emulated driver did, so the
|
||||||
|
// type change from native prepares doesn't ripple through existing code.
|
||||||
|
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||||
|
]);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
|
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ require_once __DIR__ . '/functions_migrations.php';// platform_update.php migrat
|
|||||||
require_once __DIR__ . '/functions_notify_client.php'; // notify_remote_send()
|
require_once __DIR__ . '/functions_notify_client.php'; // notify_remote_send()
|
||||||
require_once __DIR__ . '/functions_queue.php'; // enqueue_*/flush_message façade → relay
|
require_once __DIR__ . '/functions_queue.php'; // enqueue_*/flush_message façade → relay
|
||||||
require_once __DIR__ . '/functions_csrf.php';
|
require_once __DIR__ . '/functions_csrf.php';
|
||||||
|
require_once __DIR__ . '/functions_html.php'; // allowlist HTML sanitiser (clean_richtext/clean_map_embed)
|
||||||
require_once __DIR__ . '/ci_compat.php'; // CI4 view-helper shim (script_tag/link_tag/view/esc/session)
|
require_once __DIR__ . '/ci_compat.php'; // CI4 view-helper shim (script_tag/link_tag/view/esc/session)
|
||||||
require_once __DIR__ . '/functions_site.php'; // public-site data (site_info/offer/team/modalidades/horario)
|
require_once __DIR__ . '/functions_site.php'; // public-site data (site_info/offer/team/modalidades/horario)
|
||||||
require_once __DIR__ . '/functions_images.php'; // upload resize/compress (gallery/team/modalidades caps)
|
require_once __DIR__ . '/functions_images.php'; // upload resize/compress (gallery/team/modalidades caps)
|
||||||
@@ -29,6 +30,7 @@ require_once __DIR__ . '/functions_modalidades.php'; // modalidades CRUD
|
|||||||
require_once __DIR__ . '/functions_galerias.php'; // galleries + images CRUD
|
require_once __DIR__ . '/functions_galerias.php'; // galleries + images CRUD
|
||||||
require_once __DIR__ . '/functions_work_hours.php'; // work hours CRUD
|
require_once __DIR__ . '/functions_work_hours.php'; // work hours CRUD
|
||||||
require_once __DIR__ . '/functions_horario.php'; // class schedule CRUD
|
require_once __DIR__ . '/functions_horario.php'; // class schedule CRUD
|
||||||
|
require_once __DIR__ . '/functions_events.php'; // time-limited events + landing pages + leads
|
||||||
|
|
||||||
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
||||||
// Resolved once from the settings cache so call sites read plain constants.
|
// Resolved once from the settings cache so call sites read plain constants.
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<?php
|
||||||
|
// ─── Events CRUD helpers ───────────────────────────────────────────────────────
|
||||||
|
// Time-limited events that drive a homepage banner + a branded landing page at
|
||||||
|
// /evento/<slug>. Soft-delete via deleted_at (same shape as modalidades). An
|
||||||
|
// event is publicly visible only while is_active = 1 and NOW() is inside the
|
||||||
|
// [publish_from, publish_until] window. Each landing page can capture leads into
|
||||||
|
// the event_leads table. See database/migration_20260612_events.sql.
|
||||||
|
|
||||||
|
// All columns the create/update statements bind, with safe defaults. Keeps the
|
||||||
|
// view code and the two write paths in sync from one place.
|
||||||
|
function events_defaults(): array {
|
||||||
|
return [
|
||||||
|
'slug' => '', 'title' => '', 'subtitle' => '', 'summary' => '',
|
||||||
|
'description' => '', 'hero_image' => '', 'cor' => '#C2D500',
|
||||||
|
'location' => '', 'event_starts_at' => null, 'event_ends_at' => null,
|
||||||
|
'publish_from' => '', 'publish_until' => '', 'is_active' => 1,
|
||||||
|
'cta_label' => '', 'cta_url' => '', 'show_lead_form' => 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reads ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function db_events_list(PDO $pdo, bool $include_deleted = false): array {
|
||||||
|
$sql = "SELECT * FROM events";
|
||||||
|
if (!$include_deleted) $sql .= " WHERE deleted_at IS NULL";
|
||||||
|
$sql .= " ORDER BY publish_from DESC, e_id DESC";
|
||||||
|
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_get(PDO $pdo, int $id): ?array {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM events WHERE e_id = ? LIMIT 1");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_get_by_slug(PDO $pdo, string $slug): ?array {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM events WHERE slug = ? AND deleted_at IS NULL LIMIT 1");
|
||||||
|
$stmt->execute([$slug]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publicly visible events, in show order (soonest-ending first so the most
|
||||||
|
// urgent banner leads). Defensive: returns [] if the table doesn't exist yet.
|
||||||
|
function db_events_active(PDO $pdo): array {
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query(
|
||||||
|
"SELECT * FROM events
|
||||||
|
WHERE deleted_at IS NULL AND is_active = 1
|
||||||
|
AND NOW() BETWEEN publish_from AND publish_until
|
||||||
|
ORDER BY publish_until ASC, publish_from ASC"
|
||||||
|
);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle label for the backoffice badge.
|
||||||
|
function events_status(array $row): string {
|
||||||
|
if (!empty($row['deleted_at'])) return 'deleted';
|
||||||
|
if ((int) ($row['is_active'] ?? 0) !== 1) return 'inactive';
|
||||||
|
$now = time();
|
||||||
|
$from = strtotime((string) ($row['publish_from'] ?? '')) ?: 0;
|
||||||
|
$until = strtotime((string) ($row['publish_until'] ?? '')) ?: 0;
|
||||||
|
if ($from && $now < $from) return 'scheduled';
|
||||||
|
if ($until && $now > $until) return 'expired';
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the event live for the public right now?
|
||||||
|
function events_is_live(array $row): bool {
|
||||||
|
return events_status($row) === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Slug ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Build a URL-safe, unique slug from a title. When editing, pass $ignoreId so a
|
||||||
|
// row doesn't collide with itself.
|
||||||
|
function events_make_slug(PDO $pdo, string $title, ?int $ignoreId = null): string {
|
||||||
|
$base = strtolower(trim($title));
|
||||||
|
// Best-effort transliteration of accented PT characters.
|
||||||
|
if (function_exists('iconv')) {
|
||||||
|
$t = @iconv('UTF-8', 'ASCII//TRANSLIT', $base);
|
||||||
|
if ($t !== false) $base = $t;
|
||||||
|
}
|
||||||
|
$base = preg_replace('/[^a-z0-9]+/', '-', strtolower($base));
|
||||||
|
$base = trim((string) $base, '-');
|
||||||
|
if ($base === '') $base = 'evento';
|
||||||
|
$base = substr($base, 0, 150);
|
||||||
|
|
||||||
|
$slug = $base;
|
||||||
|
$i = 2;
|
||||||
|
while (true) {
|
||||||
|
if ($ignoreId !== null) {
|
||||||
|
$stmt = $pdo->prepare("SELECT 1 FROM events WHERE slug = ? AND e_id <> ? LIMIT 1");
|
||||||
|
$stmt->execute([$slug, $ignoreId]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("SELECT 1 FROM events WHERE slug = ? LIMIT 1");
|
||||||
|
$stmt->execute([$slug]);
|
||||||
|
}
|
||||||
|
if (!$stmt->fetchColumn()) return $slug;
|
||||||
|
$slug = $base . '-' . $i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Writes ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Coerce an empty datetime-local string to NULL (the optional event_* columns).
|
||||||
|
function events_nullable_dt($value): ?string {
|
||||||
|
$value = trim((string) $value);
|
||||||
|
return $value === '' ? null : str_replace('T', ' ', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_create(PDO $pdo, array $data): int {
|
||||||
|
$d = array_merge(events_defaults(), $data);
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO events
|
||||||
|
(slug, title, subtitle, summary, description, hero_image, cor, location,
|
||||||
|
event_starts_at, event_ends_at, publish_from, publish_until,
|
||||||
|
is_active, cta_label, cta_url, show_lead_form)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$d['slug'], $d['title'], $d['subtitle'], $d['summary'], $d['description'],
|
||||||
|
$d['hero_image'], $d['cor'], $d['location'],
|
||||||
|
events_nullable_dt($d['event_starts_at']), events_nullable_dt($d['event_ends_at']),
|
||||||
|
str_replace('T', ' ', (string) $d['publish_from']),
|
||||||
|
str_replace('T', ' ', (string) $d['publish_until']),
|
||||||
|
(int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'],
|
||||||
|
]);
|
||||||
|
return (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_update(PDO $pdo, int $id, array $data): bool {
|
||||||
|
$d = array_merge(events_defaults(), $data);
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE events SET
|
||||||
|
slug = ?, title = ?, subtitle = ?, summary = ?, description = ?,
|
||||||
|
hero_image = ?, cor = ?, location = ?,
|
||||||
|
event_starts_at = ?, event_ends_at = ?, publish_from = ?, publish_until = ?,
|
||||||
|
is_active = ?, cta_label = ?, cta_url = ?, show_lead_form = ?
|
||||||
|
WHERE e_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$d['slug'], $d['title'], $d['subtitle'], $d['summary'], $d['description'],
|
||||||
|
$d['hero_image'], $d['cor'], $d['location'],
|
||||||
|
events_nullable_dt($d['event_starts_at']), events_nullable_dt($d['event_ends_at']),
|
||||||
|
str_replace('T', ' ', (string) $d['publish_from']),
|
||||||
|
str_replace('T', ' ', (string) $d['publish_until']),
|
||||||
|
(int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'],
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_delete(PDO $pdo, int $id): bool {
|
||||||
|
$stmt = $pdo->prepare("UPDATE events SET deleted_at = NOW() WHERE e_id = ? AND deleted_at IS NULL");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_events_restore(PDO $pdo, int $id): bool {
|
||||||
|
$stmt = $pdo->prepare("UPDATE events SET deleted_at = NULL WHERE e_id = ? AND deleted_at IS NOT NULL");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Hero image upload / removal ───────────────────────────────────────────────
|
||||||
|
// Stored in uploads/events/ as `upl_<slug>_<rand>.<ext>`. Mirrors the modalidades
|
||||||
|
// icon upload, but optimises to the larger IMG_MAX_EVENT cap (it's a full-bleed
|
||||||
|
// hero). events_image_remove() only ever unlinks files it created (`upl_` prefix).
|
||||||
|
|
||||||
|
function events_image_dir(): string {
|
||||||
|
return __DIR__ . '/../uploads/events/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function events_image_upload(array $file, ?string &$error = null): ?string {
|
||||||
|
$error = null;
|
||||||
|
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||||
|
return null; // nothing uploaded — leave the existing image alone
|
||||||
|
}
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$error = 'Falha no upload da imagem (código ' . (int) $file['error'] . ').';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (($file['size'] ?? 0) > 6 * 1024 * 1024) {
|
||||||
|
$error = 'A imagem excede o limite de 6 MB.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
||||||
|
$error = 'Upload inválido.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$allowed = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
|
||||||
|
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($ext, $allowed, true)) {
|
||||||
|
$error = 'Formato inválido. Permitidos: ' . implode(', ', $allowed) . '.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = events_image_dir();
|
||||||
|
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
||||||
|
|
||||||
|
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower(pathinfo($file['name'], PATHINFO_FILENAME)));
|
||||||
|
$slug = substr(trim((string) $slug, '-'), 0, 40);
|
||||||
|
if ($slug === '') $slug = 'hero';
|
||||||
|
$filename = 'upl_' . $slug . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $dir . $filename)) {
|
||||||
|
$error = 'Não foi possível guardar a imagem.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (defined('IMG_MAX_EVENT') && function_exists('img_optimize')) {
|
||||||
|
img_optimize($dir . $filename, IMG_MAX_EVENT);
|
||||||
|
}
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
function events_image_remove(?string $filename): void {
|
||||||
|
$filename = basename((string) $filename);
|
||||||
|
if ($filename === '' || strncmp($filename, 'upl_', 4) !== 0) return;
|
||||||
|
$path = events_image_dir() . $filename;
|
||||||
|
if (is_file($path)) @unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Leads ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function db_event_leads_list(PDO $pdo, int $eventId): array {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM event_leads WHERE e_id = ? ORDER BY created_at DESC, lead_id DESC");
|
||||||
|
$stmt->execute([$eventId]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_event_leads_count(PDO $pdo, int $eventId): int {
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM event_leads WHERE e_id = ?");
|
||||||
|
$stmt->execute([$eventId]);
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_event_lead_create(PDO $pdo, int $eventId, array $data): int {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO event_leads (e_id, name, email, phone, message, consent, consent_text, ip, user_agent)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$eventId,
|
||||||
|
mb_substr(trim((string) ($data['name'] ?? '')), 0, 150),
|
||||||
|
mb_substr(trim((string) ($data['email'] ?? '')), 0, 190),
|
||||||
|
mb_substr(trim((string) ($data['phone'] ?? '')), 0, 40),
|
||||||
|
mb_substr(trim((string) ($data['message'] ?? '')), 0, 1000),
|
||||||
|
!empty($data['consent']) ? 1 : 0,
|
||||||
|
mb_substr(trim((string) ($data['consent_text'] ?? '')), 0, 255),
|
||||||
|
mb_substr(trim((string) ($data['ip'] ?? '')), 0, 45),
|
||||||
|
mb_substr(trim((string) ($data['user_agent'] ?? '')), 0, 255),
|
||||||
|
]);
|
||||||
|
return (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
function db_event_lead_delete(PDO $pdo, int $leadId, int $eventId): bool {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM event_leads WHERE lead_id = ? AND e_id = ?");
|
||||||
|
$stmt->execute([$leadId, $eventId]);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
// ─── HTML sanitisation ───────────────────────────────────────────────────────
|
||||||
|
// Admin-authored rich text (Summernote) and the Google-Maps embed are rendered
|
||||||
|
// raw on the public site. To keep that from being a stored-XSS vector if an
|
||||||
|
// admin/staff account is ever misused, every such value is passed through this
|
||||||
|
// allowlist sanitiser at OUTPUT time (covers legacy rows too, not just new saves).
|
||||||
|
//
|
||||||
|
// Dependency-free: uses ext-dom (libxml). Falls back to a strip_tags allowlist
|
||||||
|
// if DOM is unavailable. Disallowed elements are dropped wholesale; on every
|
||||||
|
// surviving element only whitelisted attributes remain, and href/src schemes
|
||||||
|
// are validated (no javascript:/data:/vbscript:).
|
||||||
|
//
|
||||||
|
// clean_richtext($html) — modalidades.descricao, events.description, …
|
||||||
|
// clean_map_embed($html) — fitme3070_info.google_maps (single Google iframe)
|
||||||
|
|
||||||
|
// Tags allowed in general rich text (no attributes unless listed).
|
||||||
|
const HTML_RICHTEXT_TAGS = [
|
||||||
|
'p' => [], 'br' => [], 'hr' => [], 'div' => [], 'span' => [],
|
||||||
|
'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'u' => [], 's' => [],
|
||||||
|
'ul' => [], 'ol' => [], 'li' => [],
|
||||||
|
'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [],
|
||||||
|
'blockquote' => [], 'a' => ['href', 'target', 'rel'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Elements removed entirely (content and all) when they appear anywhere.
|
||||||
|
const HTML_DANGEROUS_TAGS = [
|
||||||
|
'script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button',
|
||||||
|
'textarea', 'select', 'option', 'link', 'meta', 'base', 'svg', 'math',
|
||||||
|
'audio', 'video', 'source', 'frame', 'frameset', 'applet',
|
||||||
|
];
|
||||||
|
|
||||||
|
function clean_richtext(?string $html): string {
|
||||||
|
return html_sanitize((string) $html, HTML_RICHTEXT_TAGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The maps field holds a single Google Maps <iframe> embed. Allow exactly that.
|
||||||
|
function clean_map_embed(?string $html): string {
|
||||||
|
$allowed = [
|
||||||
|
'iframe' => ['src', 'width', 'height', 'style', 'loading',
|
||||||
|
'allowfullscreen', 'referrerpolicy', 'title'],
|
||||||
|
];
|
||||||
|
return html_sanitize((string) $html, $allowed, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core sanitiser. $allowed maps tag => list of permitted attributes.
|
||||||
|
// $allowMapsIframe gates iframe src to Google-Maps embed URLs only.
|
||||||
|
function html_sanitize(string $html, array $allowed, bool $allowMapsIframe = false): string {
|
||||||
|
$html = trim($html);
|
||||||
|
if ($html === '') return '';
|
||||||
|
|
||||||
|
if (!class_exists('DOMDocument')) {
|
||||||
|
// Conservative fallback: keep a safe inline tag set, drop ALL attributes
|
||||||
|
// (strip_tags removes attributes with the tags it keeps's contents intact).
|
||||||
|
return strip_tags($html, '<p><br><strong><b><em><i><u><ul><ol><li><h2><h3><h4><blockquote><span><a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$prev = libxml_use_internal_errors(true);
|
||||||
|
// The XML encoding hint forces UTF-8; NOIMPLIED/NODEFDTD keep it a fragment
|
||||||
|
// (no <html>/<body>/doctype wrappers added).
|
||||||
|
$doc->loadHTML(
|
||||||
|
'<?xml encoding="utf-8"?><div id="__root__">' . $html . '</div>',
|
||||||
|
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NONET
|
||||||
|
);
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($prev);
|
||||||
|
|
||||||
|
$root = $doc->getElementById('__root__');
|
||||||
|
if (!$root) {
|
||||||
|
// getElementById can miss without a DTD; fall back to first div.
|
||||||
|
$root = $doc->getElementsByTagName('div')->item(0);
|
||||||
|
}
|
||||||
|
if (!$root) return '';
|
||||||
|
|
||||||
|
_html_sanitize_children($root, $allowed, $allowMapsIframe);
|
||||||
|
|
||||||
|
$out = '';
|
||||||
|
foreach (iterator_to_array($root->childNodes) as $child) {
|
||||||
|
$out .= $doc->saveHTML($child);
|
||||||
|
}
|
||||||
|
return trim($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively scrub a node's children in place.
|
||||||
|
function _html_sanitize_children(DOMNode $node, array $allowed, bool $allowMapsIframe): void {
|
||||||
|
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||||
|
if ($child->nodeType === XML_COMMENT_NODE) {
|
||||||
|
$node->removeChild($child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
||||||
|
continue; // plain text — safe (serialised escaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = strtolower($child->nodeName);
|
||||||
|
|
||||||
|
// Disallowed → remove the element and everything inside it.
|
||||||
|
if (!isset($allowed[$tag])) {
|
||||||
|
$node->removeChild($child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip every attribute that isn't explicitly permitted for this tag,
|
||||||
|
// and validate URL-bearing ones.
|
||||||
|
if ($child->hasAttributes()) {
|
||||||
|
foreach (iterator_to_array($child->attributes) as $attr) {
|
||||||
|
$name = strtolower($attr->name);
|
||||||
|
if (!in_array($name, $allowed[$tag], true)) {
|
||||||
|
$child->removeAttribute($attr->name); // drops on*, style (rich text), etc.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (($name === 'href' || $name === 'src')
|
||||||
|
&& !_html_url_is_safe($attr->value, $tag === 'iframe' && $allowMapsIframe)) {
|
||||||
|
$child->removeAttribute($attr->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harden links that open in a new tab.
|
||||||
|
if ($tag === 'a' && strtolower($child->getAttribute('target')) === '_blank') {
|
||||||
|
$child->setAttribute('rel', 'noopener noreferrer');
|
||||||
|
}
|
||||||
|
|
||||||
|
_html_sanitize_children($child, $allowed, $allowMapsIframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow http(s)/mailto/tel/anchors/relative; for iframes, only Google-Maps
|
||||||
|
// embeds. Always reject javascript:/data:/vbscript: and other schemes.
|
||||||
|
function _html_url_is_safe(string $url, bool $isMapsIframe): bool {
|
||||||
|
$url = trim($url);
|
||||||
|
if ($url === '') return false;
|
||||||
|
if (preg_match('/^\s*(javascript|data|vbscript|file):/i', $url)) return false;
|
||||||
|
|
||||||
|
if ($isMapsIframe) {
|
||||||
|
// Any Google-owned Maps URL (www.google.com/maps/embed, maps.google.pt/…).
|
||||||
|
return (bool) preg_match('#^https://([a-z0-9-]+\.)*google\.[a-z.]+/maps#i', $url);
|
||||||
|
}
|
||||||
|
if (preg_match('#^(https?:|mailto:|tel:|/|\#|\?|\.)#i', $url)) return true;
|
||||||
|
// No scheme at all → relative path (e.g. "home/contactos") → safe.
|
||||||
|
return !preg_match('#^[a-z][a-z0-9+.\-]*:#i', $url);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
const IMG_MAX_GALLERY = 1920;
|
const IMG_MAX_GALLERY = 1920;
|
||||||
const IMG_MAX_TEAM = 512;
|
const IMG_MAX_TEAM = 512;
|
||||||
const IMG_MAX_MODALIDADE = 256;
|
const IMG_MAX_MODALIDADE = 256;
|
||||||
|
const IMG_MAX_EVENT = 1600;
|
||||||
|
|
||||||
const IMG_JPEG_QUALITY = 82;
|
const IMG_JPEG_QUALITY = 82;
|
||||||
const IMG_WEBP_QUALITY = 80;
|
const IMG_WEBP_QUALITY = 80;
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ function site_modalidades(PDO $pdo): array {
|
|||||||
} catch (Throwable $e) { return []; }
|
} catch (Throwable $e) { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently-visible time-limited events for the homepage banner. Thin wrapper
|
||||||
|
// over db_events_active() (functions_events.php) so the site code reads in the
|
||||||
|
// same site_*() style as everything else here.
|
||||||
|
function site_active_events(PDO $pdo): array {
|
||||||
|
return function_exists('db_events_active') ? db_events_active($pdo) : [];
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Image-path resolver ──────────────────────────────────────────────────────
|
// ─── Image-path resolver ──────────────────────────────────────────────────────
|
||||||
// DB stores either a bare filename ("1.jpg") or a full relative path
|
// DB stores either a bare filename ("1.jpg") or a full relative path
|
||||||
// ("uploads/team/foto/1.jpg"). This normalises both to a working URL.
|
// ("uploads/team/foto/1.jpg"). This normalises both to a working URL.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
-- ─── Events / time-limited landing pages ────────────────────────────────────
|
||||||
|
-- Backoffice-managed temporary events (promotions, special classes, challenges).
|
||||||
|
-- Each row drives a banner on the homepage and a branded landing page at
|
||||||
|
-- /evento/<slug>, visible only while NOW() is inside [publish_from, publish_until]
|
||||||
|
-- and is_active = 1. Soft-deleted via deleted_at (mirrors the modalidades module).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `events` (
|
||||||
|
`e_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`deleted_at` DATETIME DEFAULT NULL,
|
||||||
|
`slug` VARCHAR(160) NOT NULL,
|
||||||
|
`title` VARCHAR(180) NOT NULL,
|
||||||
|
`subtitle` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`summary` VARCHAR(400) NOT NULL DEFAULT '',
|
||||||
|
`description` TEXT DEFAULT NULL,
|
||||||
|
`hero_image` VARCHAR(160) NOT NULL DEFAULT '',
|
||||||
|
`cor` VARCHAR(50) NOT NULL DEFAULT '#C2D500',
|
||||||
|
`location` VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
`event_starts_at` DATETIME DEFAULT NULL,
|
||||||
|
`event_ends_at` DATETIME DEFAULT NULL,
|
||||||
|
`publish_from` DATETIME NOT NULL,
|
||||||
|
`publish_until` DATETIME NOT NULL,
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`cta_label` VARCHAR(80) NOT NULL DEFAULT '',
|
||||||
|
`cta_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`show_lead_form` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`e_id`),
|
||||||
|
UNIQUE KEY `uq_events_slug` (`slug`),
|
||||||
|
KEY `idx_events_window` (`is_active`, `publish_from`, `publish_until`, `deleted_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Lead/registration submissions captured on each event landing page.
|
||||||
|
CREATE TABLE IF NOT EXISTS `event_leads` (
|
||||||
|
`lead_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`e_id` INT(11) NOT NULL,
|
||||||
|
`name` VARCHAR(150) NOT NULL,
|
||||||
|
`email` VARCHAR(190) NOT NULL DEFAULT '',
|
||||||
|
`phone` VARCHAR(40) NOT NULL DEFAULT '',
|
||||||
|
`message` VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`lead_id`),
|
||||||
|
KEY `idx_event_leads_event` (`e_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- ─── event_leads: consent + provenance + FK (D1) ────────────────────────────
|
||||||
|
-- event_leads captures PII from a public form. Under the GDPR we must record
|
||||||
|
-- demonstrable consent and minimise/retain responsibly. Add the consent flag,
|
||||||
|
-- the exact text the visitor agreed to, and request provenance (IP + UA) so a
|
||||||
|
-- submission can be evidenced. Also wire the missing FK to events so leads are
|
||||||
|
-- removed with their event (no orphans).
|
||||||
|
--
|
||||||
|
-- NB: filename sorts AFTER migration_20260612_events.sql (which creates the
|
||||||
|
-- table), so the runner applies them in the right order.
|
||||||
|
|
||||||
|
ALTER TABLE `event_leads`
|
||||||
|
ADD COLUMN IF NOT EXISTS `consent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `message`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `consent_text` VARCHAR(255) NOT NULL DEFAULT '' AFTER `consent`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `ip` VARCHAR(45) NOT NULL DEFAULT '' AFTER `consent_text`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `user_agent` VARCHAR(255) NOT NULL DEFAULT '' AFTER `ip`;
|
||||||
|
|
||||||
|
-- Referential integrity: delete leads when their event is hard-deleted.
|
||||||
|
-- (Has no IF NOT EXISTS in MariaDB; if re-running after a partial apply, drop
|
||||||
|
-- the constraint first.)
|
||||||
|
ALTER TABLE `event_leads`
|
||||||
|
ADD CONSTRAINT `fk_event_leads_event`
|
||||||
|
FOREIGN KEY (`e_id`) REFERENCES `events` (`e_id`) ON DELETE CASCADE;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- ─── Index gallery images by their parent gallery (O1) ──────────────────────
|
||||||
|
-- gallery.i_project_id is filtered on every gallery view (db_gallery_images) and
|
||||||
|
-- on delete, but had no index → full table scan. Add one.
|
||||||
|
|
||||||
|
ALTER TABLE `gallery`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_gallery_project` (`i_project_id`);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- ─── horario.hora_inicio: varchar(15) → TIME (O3) ──────────────────────────
|
||||||
|
-- The column already holds time-of-day strings ("HH:MM") and the app reads it
|
||||||
|
-- with TIME_FORMAT(hora_inicio, '%H:%i'), so it's semantically a time. Storing
|
||||||
|
-- it as TIME makes ordering/comparison correct and matches hora_fim (already
|
||||||
|
-- TIME). Existing "HH:MM" values cast cleanly. If any non-time legacy value
|
||||||
|
-- exists this ALTER will fail under strict mode — fix that row, then re-run.
|
||||||
|
|
||||||
|
ALTER TABLE `horario`
|
||||||
|
MODIFY `hora_inicio` TIME NOT NULL;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- ─── Widen the Maps columns to avoid truncation (D8) ────────────────────────
|
||||||
|
-- A Google Maps embed (<iframe …>) and a Maps share link are frequently longer
|
||||||
|
-- than the original varchar(500)/varchar(100), which silently truncated them
|
||||||
|
-- (breaking the embed). Promote to TEXT / varchar(255).
|
||||||
|
|
||||||
|
ALTER TABLE `fitme3070_info`
|
||||||
|
MODIFY `google_maps` TEXT NOT NULL DEFAULT '',
|
||||||
|
MODIFY `google_maps_link` VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- ─── Widen modalidades.descricao for rich-text (Summernote) ──────────────────
|
||||||
|
-- The backoffice now edits this field with the WYSIWYG editor, which produces
|
||||||
|
-- HTML markup and bypasses the textarea maxlength. varchar(1500) could silently
|
||||||
|
-- truncate that HTML mid-tag (and the public site renders it raw), so promote it
|
||||||
|
-- to TEXT. Existing values are preserved by the widening conversion.
|
||||||
|
|
||||||
|
ALTER TABLE `modalidades`
|
||||||
|
MODIFY `descricao` TEXT NOT NULL;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- ─── Widen team.description for rich-text (Summernote) ───────────────────────
|
||||||
|
-- The backoffice now edits this field with the WYSIWYG editor, which emits HTML
|
||||||
|
-- and bypasses the textarea maxlength. varchar(300) would silently truncate that
|
||||||
|
-- markup mid-tag, so promote it to TEXT. Stays nullable; existing values kept.
|
||||||
|
|
||||||
|
ALTER TABLE `team`
|
||||||
|
MODIFY `description` TEXT DEFAULT NULL;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- ─── Marketing-consent provenance on users (D2) ─────────────────────────────
|
||||||
|
-- `marketing_consent` is a bare boolean — the GDPR requires demonstrable consent
|
||||||
|
-- (when and how it was given). Add a timestamp and a source so consent can be
|
||||||
|
-- proven and audited. Nullable: existing rows have unknown provenance.
|
||||||
|
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN IF NOT EXISTS `marketing_consent_at` DATETIME DEFAULT NULL AFTER `marketing_consent`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `marketing_consent_source` VARCHAR(60) DEFAULT NULL AFTER `marketing_consent_at`;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- ─── website_offer soft-delete: drop the zero-date (D4) ─────────────────────
|
||||||
|
-- o_deleted_at was datetime NOT NULL, so "not deleted" rows carried the invalid
|
||||||
|
-- zero-date '0000-00-00 00:00:00' (rejected under NO_ZERO_DATE/strict mode) and
|
||||||
|
-- the app had to filter for it explicitly. Make it nullable and normalise the
|
||||||
|
-- existing zero-dates to NULL, aligning with every other table's deleted_at.
|
||||||
|
|
||||||
|
ALTER TABLE `website_offer`
|
||||||
|
MODIFY `o_deleted_at` DATETIME NULL DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Normalise legacy "not deleted" markers to NULL. Comparing against a literal
|
||||||
|
-- zero-date is itself unsafe under strict mode, so match by year 0 instead.
|
||||||
|
UPDATE `website_offer`
|
||||||
|
SET `o_deleted_at` = NULL
|
||||||
|
WHERE `o_deleted_at` IS NOT NULL
|
||||||
|
AND YEAR(`o_deleted_at`) = 0;
|
||||||
@@ -3,7 +3,55 @@
|
|||||||
?>
|
?>
|
||||||
</div><!-- /.p-3 / .p-md-4 -->
|
</div><!-- /.p-3 / .p-md-4 -->
|
||||||
</main>
|
</main>
|
||||||
|
<?= script_tag('assets/vendor/jquery/jquery-3.7.1.min.js') ?>
|
||||||
<?= script_tag('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') ?>
|
<?= script_tag('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') ?>
|
||||||
|
<?= script_tag('assets/vendor/summernote/summernote-bs5.min.js') ?>
|
||||||
|
<?= script_tag('assets/vendor/summernote/lang/summernote-pt-PT.min.js') ?>
|
||||||
|
<script>
|
||||||
|
// Attach Summernote to any <textarea class="js-wysiwyg">. The editor keeps the
|
||||||
|
// underlying textarea in sync, so normal form POSTs submit the generated HTML.
|
||||||
|
// Inline image upload is intentionally omitted (base64 would bloat the row) —
|
||||||
|
// modules that need images use their own file-upload fields.
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
if (!window.jQuery || !jQuery.fn || !jQuery.fn.summernote) return;
|
||||||
|
jQuery('textarea.js-wysiwyg').each(function () {
|
||||||
|
var $ta = jQuery(this);
|
||||||
|
$ta.summernote({
|
||||||
|
lang: 'pt-PT',
|
||||||
|
height: parseInt($ta.data('wysiwyg-height'), 10) || 240,
|
||||||
|
disableDragAndDrop: true,
|
||||||
|
toolbar: [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'italic', 'underline', 'clear']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['insert', ['link']],
|
||||||
|
['view', ['codeview', 'fullscreen']]
|
||||||
|
],
|
||||||
|
styleTags: ['p', 'h2', 'h3', 'h4', 'blockquote'],
|
||||||
|
popover: { image: [], air: [] }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile off-canvas sidebar: hamburger toggles .sidebar.open + the backdrop;
|
||||||
|
// tapping the backdrop, a nav link, Escape, or resizing to desktop closes it.
|
||||||
|
(function () {
|
||||||
|
var sb = document.getElementById('sidebar');
|
||||||
|
var tg = document.getElementById('menuToggle');
|
||||||
|
var bd = document.getElementById('sidebarBackdrop');
|
||||||
|
if (!sb || !tg) return;
|
||||||
|
var mq = window.matchMedia('(max-width: 991.98px)');
|
||||||
|
function open() { sb.classList.add('open'); if (bd) bd.classList.add('show'); document.body.style.overflow = 'hidden'; tg.setAttribute('aria-expanded', 'true'); }
|
||||||
|
function close() { sb.classList.remove('open'); if (bd) bd.classList.remove('show'); document.body.style.overflow = ''; tg.setAttribute('aria-expanded', 'false'); }
|
||||||
|
tg.addEventListener('click', function () { sb.classList.contains('open') ? close() : open(); });
|
||||||
|
if (bd) bd.addEventListener('click', close);
|
||||||
|
sb.querySelectorAll('.nav-link').forEach(function (a) {
|
||||||
|
a.addEventListener('click', function () { if (mq.matches) close(); });
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||||
|
window.addEventListener('resize', function () { if (!mq.matches) close(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* Console footer — subtle brand bar */
|
/* Console footer — subtle brand bar */
|
||||||
main:after {
|
main:after {
|
||||||
|
|||||||
+46
-4
@@ -13,6 +13,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
<title><?= htmlspecialchars($page_title) ?> — <?= htmlspecialchars($app_name) ?></title>
|
<title><?= htmlspecialchars($page_title) ?> — <?= htmlspecialchars($app_name) ?></title>
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
||||||
|
<link href="<?= base_url('assets/vendor/summernote/summernote-bs5.min.css') ?>" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bo-accent: #C2D500;
|
--bo-accent: #C2D500;
|
||||||
@@ -39,7 +40,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
.topbar { position: fixed; top: 0; left: 240px; right: 0; z-index: 99; height: 56px;
|
.topbar { position: fixed; top: 0; left: 240px; right: 0; z-index: 99; height: 56px;
|
||||||
background: #fff; border-bottom: 1px solid #e2e6ec; padding: 0 1.5rem;
|
background: #fff; border-bottom: 1px solid #e2e6ec; padding: 0 1.5rem;
|
||||||
display: flex; align-items: center; justify-content: space-between; }
|
display: flex; align-items: center; justify-content: space-between; }
|
||||||
.topbar .user-dropdown { display: flex; align-items: center; gap: .5rem; }
|
.topbar .user-dropdown { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
|
||||||
.badge-role-sysadmin { background: var(--bo-accent); color: var(--bo-dark); }
|
.badge-role-sysadmin { background: var(--bo-accent); color: var(--bo-dark); }
|
||||||
.badge-role-admin { background: var(--bo-accent); color: var(--bo-dark); opacity: .85; }
|
.badge-role-admin { background: var(--bo-accent); color: var(--bo-dark); opacity: .85; }
|
||||||
.badge-role-staff { background: #6f42c1; }
|
.badge-role-staff { background: #6f42c1; }
|
||||||
@@ -50,10 +51,44 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
.btn-primary:hover { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
|
.btn-primary:hover { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
|
||||||
.card { border: none; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
.card { border: none; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
||||||
a { color: #0a0a0a; }
|
a { color: #0a0a0a; }
|
||||||
|
/* Summernote (WYSIWYG) — align with the backoffice look */
|
||||||
|
.note-editor.note-frame { border-color: #ced4da; border-radius: .375rem; }
|
||||||
|
.note-editor .note-editing-area .note-editable { font-size: .95rem; }
|
||||||
|
.note-editor.note-frame.note-focused { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
|
||||||
|
.note-toolbar { background: #f8f9fa; }
|
||||||
|
.note-btn.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
||||||
|
/* ── Mobile: off-canvas sidebar + hamburger ──────────────────────────── */
|
||||||
|
.sidebar { transition: transform .25s ease; }
|
||||||
|
.menu-toggle { display: none; width: 40px; height: 40px; padding: 0; border: 1px solid #e2e6ec;
|
||||||
|
background: #fff; color: var(--bo-dark); border-radius: 8px; font-size: 1.35rem;
|
||||||
|
line-height: 1; align-items: center; justify-content: center; }
|
||||||
|
.sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 1090; background: rgba(0,0,0,.45); }
|
||||||
|
.sidebar-backdrop.show { display: block; }
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.sidebar { transform: translateX(-100%); width: 264px; z-index: 1100;
|
||||||
|
box-shadow: 2px 0 18px rgba(0,0,0,.3); }
|
||||||
|
.sidebar .sidebar-brand { width: 264px; z-index: 1101; }
|
||||||
|
.sidebar.open { transform: translateX(0); }
|
||||||
|
main { margin-left: 0; }
|
||||||
|
.topbar { left: 0; padding: 0 .75rem; }
|
||||||
|
.menu-toggle { display: inline-flex; }
|
||||||
|
/* Wide tables scroll inside their card instead of overflowing the page */
|
||||||
|
main .card > .card-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
|
}
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.topbar .user-dropdown .badge,
|
||||||
|
.topbar .user-dropdown .u-name { display: none; }
|
||||||
|
/* Page-header rows (title + action buttons) stack instead of overflowing */
|
||||||
|
main .d-flex.justify-content-between.align-items-center { flex-wrap: wrap; gap: .5rem; }
|
||||||
|
/* 16px inputs stop iOS from auto-zooming on focus */
|
||||||
|
.form-control, .form-select { font-size: 16px; }
|
||||||
|
/* Comfortable content gutters on phones */
|
||||||
|
.p-3 { padding: .85rem !important; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="sidebar">
|
<nav class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-brand"><i class="bi bi-lightning-charge-fill me-2"></i><?= htmlspecialchars($app_name) ?></div>
|
<div class="sidebar-brand"><i class="bi bi-lightning-charge-fill me-2"></i><?= htmlspecialchars($app_name) ?></div>
|
||||||
<div class="sidebar-section">Principal</div>
|
<div class="sidebar-section">Principal</div>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
@@ -83,6 +118,10 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/galerias/') !== false) ? ' active' : '' ?>"
|
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/galerias/') !== false) ? ' active' : '' ?>"
|
||||||
href="<?= base_url('views/admin/galerias/index.php') ?>"><i class="bi bi-images"></i>Galerias</a>
|
href="<?= base_url('views/admin/galerias/index.php') ?>"><i class="bi bi-images"></i>Galerias</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/events/') !== false) ? ' active' : '' ?>"
|
||||||
|
href="<?= base_url('views/admin/events/index.php') ?>"><i class="bi bi-megaphone-fill"></i>Eventos</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/work_hours/') !== false) ? ' active' : '' ?>"
|
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/work_hours/') !== false) ? ' active' : '' ?>"
|
||||||
href="<?= base_url('views/admin/work_hours/index.php') ?>"><i class="bi bi-clock-fill"></i>Horário de Funcionamento</a>
|
href="<?= base_url('views/admin/work_hours/index.php') ?>"><i class="bi bi-clock-fill"></i>Horário de Funcionamento</a>
|
||||||
@@ -122,15 +161,18 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
</ul>
|
</ul>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="sidebar-backdrop" id="sidebarBackdrop" aria-hidden="true"></div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<div></div>
|
<button type="button" class="menu-toggle" id="menuToggle" aria-label="Abrir menu" aria-controls="sidebar">
|
||||||
|
<i class="bi bi-list"></i>
|
||||||
|
</button>
|
||||||
<div class="user-dropdown">
|
<div class="user-dropdown">
|
||||||
<span class="badge rounded-pill badge-role-<?= htmlspecialchars($user['role']) ?>">
|
<span class="badge rounded-pill badge-role-<?= htmlspecialchars($user['role']) ?>">
|
||||||
<?= htmlspecialchars(role_label($user['role'])) ?>
|
<?= htmlspecialchars(role_label($user['role'])) ?>
|
||||||
</span>
|
</span>
|
||||||
<span><?= htmlspecialchars($user['name']) ?></span>
|
<span class="u-name"><?= htmlspecialchars($user['name']) ?></span>
|
||||||
<a href="<?= base_url('views/user/profile.php') ?>" class="btn btn-sm btn-outline-secondary"><i class="bi bi-person"></i></a>
|
<a href="<?= base_url('views/user/profile.php') ?>" class="btn btn-sm btn-outline-secondary"><i class="bi bi-person"></i></a>
|
||||||
<a href="<?= base_url('views/auth/logout.php') ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-box-arrow-right"></i></a>
|
<a href="<?= base_url('views/auth/logout.php') ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-box-arrow-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2634,4 +2634,282 @@ section[id] {
|
|||||||
|
|
||||||
.trial-form .site-btn {
|
.trial-form .site-btn {
|
||||||
border: none;
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════════════
|
||||||
|
Events — homepage banner + landing page (/evento/<slug>)
|
||||||
|
══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Active events — featured inside the hero/header ───────────────────────── */
|
||||||
|
.event-live-dot {
|
||||||
|
width: 9px; height: 9px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--fitme-accent);
|
||||||
|
box-shadow: 0 0 0 0 rgba(var(--fitme-accent-rgb), .7);
|
||||||
|
animation: event-pulse 1.8s infinite;
|
||||||
|
}
|
||||||
|
@keyframes event-pulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(var(--fitme-accent-rgb), .7); }
|
||||||
|
70% { box-shadow: 0 0 0 10px rgba(var(--fitme-accent-rgb), 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(var(--fitme-accent-rgb), 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-events {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 1.75rem;
|
||||||
|
}
|
||||||
|
.hero-event {
|
||||||
|
--ev-accent: var(--fitme-accent);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: min(640px, 92vw);
|
||||||
|
padding: 12px 14px 12px 18px;
|
||||||
|
background: rgba(11, 11, 13, .55);
|
||||||
|
border: 1.5px solid rgba(var(--fitme-accent-rgb), .55);
|
||||||
|
border-radius: 999px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
animation: hero-event-glow 2.8s ease-in-out infinite;
|
||||||
|
transition: transform var(--fitme-transition), box-shadow var(--fitme-transition), border-color var(--fitme-transition);
|
||||||
|
}
|
||||||
|
/* The event's own image as a frosted (blurred + darkened) backdrop inside the
|
||||||
|
pill — only when --ev-img is set; otherwise the translucent dark bg shows. */
|
||||||
|
.hero-event.has-img::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
background-image: var(--ev-img);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(6px) saturate(1.15) brightness(.5);
|
||||||
|
transform: scale(1.18); /* hide the blur's transparent edges */
|
||||||
|
transition: transform var(--fitme-transition);
|
||||||
|
}
|
||||||
|
.hero-event.has-img:hover::before { transform: scale(1.24); }
|
||||||
|
/* Keep the content above the image layer. */
|
||||||
|
.hero-event > * { position: relative; z-index: 1; }
|
||||||
|
@keyframes hero-event-glow {
|
||||||
|
0%, 100% { box-shadow: 0 10px 34px rgba(0,0,0,.45), 0 0 18px rgba(var(--fitme-accent-rgb), .14); }
|
||||||
|
50% { box-shadow: 0 10px 34px rgba(0,0,0,.45), 0 0 30px rgba(var(--fitme-accent-rgb), .32); }
|
||||||
|
}
|
||||||
|
.hero-event:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--ev-accent);
|
||||||
|
box-shadow: 0 16px 44px rgba(0, 0, 0, .55), 0 0 34px rgba(var(--fitme-accent-rgb), .38);
|
||||||
|
}
|
||||||
|
.hero-event-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 7px 13px;
|
||||||
|
font-size: .72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--fitme-accent);
|
||||||
|
background: rgba(var(--fitme-accent-rgb), .14);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.hero-event-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
.hero-event-title {
|
||||||
|
font-size: 1.12rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.hero-event-sub {
|
||||||
|
font-size: .9rem;
|
||||||
|
color: rgba(255, 255, 255, .72);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.hero-event-go {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 11px 20px;
|
||||||
|
font-size: .92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--fitme-dark);
|
||||||
|
background: var(--ev-accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.hero-event-go i { transition: transform var(--fitme-transition); }
|
||||||
|
.hero-event:hover .hero-event-go i { transform: translateX(4px); }
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.hero-event {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 18px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.hero-event-text { flex-basis: 100%; align-items: center; }
|
||||||
|
.hero-event-title, .hero-event-sub { white-space: normal; }
|
||||||
|
.hero-event-go { flex-basis: 100%; justify-content: center; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.hero-event { animation: none; }
|
||||||
|
.event-live-dot { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Staff preview ribbon ──────────────────────────────────────────────────── */
|
||||||
|
.event-preview-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1080;
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0B0B0D;
|
||||||
|
background: #ffd84d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Landing hero ──────────────────────────────────────────────────────────── */
|
||||||
|
.event-hero {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 70vh;
|
||||||
|
padding: 160px 0 90px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.event-hero--plain {
|
||||||
|
background: linear-gradient(135deg, var(--fitme-dark) 0%, var(--fitme-dark-2) 100%);
|
||||||
|
}
|
||||||
|
.event-hero-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(180deg, rgba(11,11,13,.35) 0%, rgba(11,11,13,.78) 100%);
|
||||||
|
}
|
||||||
|
.event-hero .container { position: relative; z-index: 2; }
|
||||||
|
.event-hero-inner { max-width: 760px; }
|
||||||
|
.event-hero-eyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.event-hero-inner h1 {
|
||||||
|
font-size: clamp(2.2rem, 5vw, 3.6rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.event-hero-sub {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255,255,255,.85);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.event-hero-meta {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 26px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 26px;
|
||||||
|
}
|
||||||
|
.event-hero-meta li {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(255,255,255,.9);
|
||||||
|
}
|
||||||
|
.event-hero-meta i { margin-right: 8px; opacity: .85; }
|
||||||
|
.event-hero-cta { border: none; color: var(--fitme-dark) !important; }
|
||||||
|
|
||||||
|
/* ── Landing body (admin HTML) ─────────────────────────────────────────────── */
|
||||||
|
.event-body { padding: 70px 0; background: var(--fitme-white); }
|
||||||
|
.event-body p { font-size: 1.05rem; line-height: 1.7; color: var(--fitme-text-soft); }
|
||||||
|
.event-body h2, .event-body h3, .event-body h4 { margin-top: 1.6rem; }
|
||||||
|
.event-body img { max-width: 100%; height: auto; border-radius: var(--fitme-radius); }
|
||||||
|
|
||||||
|
/* ── Lead form ─────────────────────────────────────────────────────────────── */
|
||||||
|
.event-leadform { padding: 70px 0; background: var(--fitme-surface); }
|
||||||
|
.event-form { margin-top: 26px; }
|
||||||
|
.event-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.event-field--full { grid-column: 1 / -1; }
|
||||||
|
.event-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--fitme-text-soft);
|
||||||
|
}
|
||||||
|
.event-field input,
|
||||||
|
.event-field textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--fitme-white);
|
||||||
|
border: 1px solid var(--fitme-surface-dark);
|
||||||
|
border-radius: var(--fitme-radius);
|
||||||
|
color: var(--fitme-text);
|
||||||
|
}
|
||||||
|
.event-field input:focus,
|
||||||
|
.event-field textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--fitme-accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--fitme-accent-rgb), .2);
|
||||||
|
}
|
||||||
|
/* honeypot — visually hidden, kept in the layout flow for bots */
|
||||||
|
.event-hp {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -9999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.event-alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: var(--fitme-radius);
|
||||||
|
font-size: .95rem;
|
||||||
|
}
|
||||||
|
.event-alert--ok { background: #e6f6ea; color: #1c6b34; border: 1px solid #9fdcb0; }
|
||||||
|
.event-alert--err { background: #fdecec; color: #a32424; border: 1px solid #f1b5b5; }
|
||||||
|
.event-consent {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 9px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: .88rem;
|
||||||
|
color: var(--fitme-text-soft);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.event-consent input { margin-top: 3px; flex: 0 0 auto; }
|
||||||
|
.event-consent a { color: var(--fitme-accent-dark); text-decoration: underline; }
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.event-form-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
// ── Event landing page ──────────────────────────────────────────────────────
|
||||||
|
// Branded, time-limited landing page reached at /evento/<slug> (see .htaccess).
|
||||||
|
// Visible to the public only while the event is live (db_events_active rule);
|
||||||
|
// logged-in staff may preview drafts/scheduled/expired events. Carries an
|
||||||
|
// optional lead-capture form that stores submissions in event_leads.
|
||||||
|
require_once __DIR__ . '/../config/db.php';
|
||||||
|
require_once __DIR__ . '/../config/functions.php';
|
||||||
|
|
||||||
|
// Resolve any SSO session up front (mirrors _open.php) so the is_staff() preview
|
||||||
|
// gate below sees a logged-in admin. _open.php's later call then no-ops.
|
||||||
|
if (empty($_SESSION['user_id']) && identity_is_federated()) {
|
||||||
|
sso_resolve_session($pdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = preg_replace('/[^a-z0-9\-]/', '', strtolower((string) ($_GET['slug'] ?? '')));
|
||||||
|
$event = $slug !== '' ? db_events_get_by_slug($pdo, $slug) : null;
|
||||||
|
|
||||||
|
// Gate: live for everyone, otherwise only staff (preview).
|
||||||
|
$can_view = $event && (events_is_live($event) || is_staff());
|
||||||
|
|
||||||
|
// ── Lead form submission (PRG) ──────────────────────────────────────────────
|
||||||
|
$lead_errors = [];
|
||||||
|
if ($can_view && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
csrf_check();
|
||||||
|
$back = base_url('evento/' . rawurlencode($event['slug']));
|
||||||
|
|
||||||
|
// Honeypot: a filled "website" field means a bot — silently accept & drop.
|
||||||
|
if (trim((string) ($_POST['website'] ?? '')) !== '') {
|
||||||
|
header('Location: ' . $back . '?ok=1#inscricao');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exact wording the visitor consents to — stored for GDPR evidence.
|
||||||
|
$consent_text = 'Autorizo o contacto e o tratamento dos meus dados para efeitos desta inscrição, nos termos da Política de Privacidade.';
|
||||||
|
$lead = [
|
||||||
|
'name' => trim((string) ($_POST['lead_name'] ?? '')),
|
||||||
|
'email' => trim((string) ($_POST['lead_email'] ?? '')),
|
||||||
|
'phone' => trim((string) ($_POST['lead_phone'] ?? '')),
|
||||||
|
'message' => trim((string) ($_POST['lead_message'] ?? '')),
|
||||||
|
'consent' => !empty($_POST['lead_consent']) ? 1 : 0,
|
||||||
|
'consent_text' => $consent_text,
|
||||||
|
// First hop of X-Forwarded-For (behind the id.andmore.pt proxy), else peer.
|
||||||
|
'ip' => trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''))[0]) ?: ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||||
|
'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
|
||||||
|
];
|
||||||
|
if ($lead['name'] === '') $lead_errors[] = 'Indica o teu nome.';
|
||||||
|
if ($lead['email'] === '' && $lead['phone'] === '') $lead_errors[] = 'Indica um email ou um telefone para te podermos contactar.';
|
||||||
|
if ($lead['email'] !== '' && !filter_var($lead['email'], FILTER_VALIDATE_EMAIL)) $lead_errors[] = 'O email indicado não é válido.';
|
||||||
|
if (!$lead['consent']) $lead_errors[] = 'É necessário aceitar o tratamento dos dados para enviar a inscrição.';
|
||||||
|
|
||||||
|
if (!$lead_errors) {
|
||||||
|
db_event_lead_create($pdo, (int) $event['e_id'], $lead);
|
||||||
|
header('Location: ' . $back . '?ok=1#inscricao');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// fall through and re-render with $lead_errors + the submitted $lead values
|
||||||
|
$lead_old = $lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Not found / not visible ─────────────────────────────────────────────────
|
||||||
|
if (!$can_view) {
|
||||||
|
http_response_code(404);
|
||||||
|
$current_uri_2 = '';
|
||||||
|
require __DIR__ . '/_open.php';
|
||||||
|
?>
|
||||||
|
<section class="event-notfound" style="padding:160px 0 90px;text-align:center;">
|
||||||
|
<div class="container">
|
||||||
|
<h1 style="font-size:2.4rem;">Evento não encontrado</h1>
|
||||||
|
<p class="text-muted">Este evento pode já ter terminado ou o link estar incorreto.</p>
|
||||||
|
<a href="<?= base_url('/') ?>" class="site-btn mt-3">Voltar ao início</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
echo view('footer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Visible: render the landing page ────────────────────────────────────────
|
||||||
|
$is_preview = !events_is_live($event);
|
||||||
|
$hero_url = $event['hero_image'] !== '' ? base_url('uploads/events/' . rawurlencode($event['hero_image'])) : '';
|
||||||
|
$accent = $event['cor'] !== '' ? $event['cor'] : '#C2D500';
|
||||||
|
|
||||||
|
// Human date line for the event window.
|
||||||
|
$date_line = '';
|
||||||
|
if (!empty($event['event_starts_at'])) {
|
||||||
|
$date_line = format_date_pt((string) $event['event_starts_at']);
|
||||||
|
if (!empty($event['event_ends_at'])
|
||||||
|
&& substr((string) $event['event_ends_at'], 0, 10) !== substr((string) $event['event_starts_at'], 0, 10)) {
|
||||||
|
$date_line .= ' – ' . format_date_pt((string) $event['event_ends_at']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CTA target: explicit URL, else the on-page form (when shown), else contacts.
|
||||||
|
$cta_label = trim((string) $event['cta_label']);
|
||||||
|
$cta_href = trim((string) $event['cta_url']);
|
||||||
|
if ($cta_href === '') {
|
||||||
|
$cta_href = ((int) $event['show_lead_form'] === 1) ? '#inscricao' : base_url('home/contactos');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SEO ─────────────────────────────────────────────────────────────────────
|
||||||
|
// include.php / seo.php honour these globals when set before _open.php runs.
|
||||||
|
$event_url = base_url('evento/' . rawurlencode($event['slug']));
|
||||||
|
$seo_desc = $event['summary'] !== ''
|
||||||
|
? $event['summary']
|
||||||
|
: trim(mb_substr(strip_tags((string) ($event['subtitle'] ?: $event['description'])), 0, 200));
|
||||||
|
|
||||||
|
$current_uri_2 = 'evento';
|
||||||
|
$GLOBALS['_page_meta_override'] = [
|
||||||
|
($event['title'] !== '' ? $event['title'] : 'Evento') . ' | FitMe 3070',
|
||||||
|
$seo_desc,
|
||||||
|
];
|
||||||
|
$GLOBALS['_canonical_override'] = $event_url;
|
||||||
|
$GLOBALS['_og_type_override'] = 'article';
|
||||||
|
if ($hero_url !== '') $GLOBALS['_og_image_override'] = $hero_url;
|
||||||
|
|
||||||
|
// Event structured data (schema.org/Event) for rich link previews / search.
|
||||||
|
$info = site_info($pdo);
|
||||||
|
$ld_event = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'Event',
|
||||||
|
'name' => $event['title'],
|
||||||
|
'description' => $seo_desc !== '' ? $seo_desc : $event['title'],
|
||||||
|
'url' => $event_url,
|
||||||
|
'eventStatus' => 'https://schema.org/EventScheduled',
|
||||||
|
'eventAttendanceMode' => 'https://schema.org/OfflineEventAttendanceMode',
|
||||||
|
];
|
||||||
|
if ($hero_url !== '') $ld_event['image'] = [$hero_url];
|
||||||
|
if (!empty($event['event_starts_at'])) $ld_event['startDate'] = str_replace(' ', 'T', (string) $event['event_starts_at']);
|
||||||
|
if (!empty($event['event_ends_at'])) $ld_event['endDate'] = str_replace(' ', 'T', (string) $event['event_ends_at']);
|
||||||
|
$ld_event['location'] = $event['location'] !== ''
|
||||||
|
? ['@type' => 'Place', 'name' => $event['location']]
|
||||||
|
: ['@type' => 'Place',
|
||||||
|
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
|
||||||
|
'address' => $info['adress'] ?? ''];
|
||||||
|
$ld_event['organizer'] = [
|
||||||
|
'@type' => 'Organization',
|
||||||
|
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
|
||||||
|
'url' => base_url(),
|
||||||
|
];
|
||||||
|
$GLOBALS['_jsonld_extra'] = $ld_event;
|
||||||
|
|
||||||
|
require __DIR__ . '/_open.php';
|
||||||
|
?>
|
||||||
|
<?php if ($is_preview): ?>
|
||||||
|
<div class="event-preview-bar">Pré-visualização — este evento não está visível ao público (<?= htmlspecialchars(events_status($event)) ?>).</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="event-hero set-bg<?= $hero_url ? '' : ' event-hero--plain' ?>"
|
||||||
|
<?= $hero_url ? 'style="background-image:url(\'' . htmlspecialchars($hero_url, ENT_QUOTES) . '\')"' : '' ?>>
|
||||||
|
<div class="event-hero-overlay"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="event-hero-inner">
|
||||||
|
<span class="event-hero-eyebrow" style="color:<?= htmlspecialchars($accent, ENT_QUOTES) ?>">Evento</span>
|
||||||
|
<h1><?= htmlspecialchars($event['title']) ?></h1>
|
||||||
|
<?php if ($event['subtitle'] !== ''): ?><p class="event-hero-sub"><?= htmlspecialchars($event['subtitle']) ?></p><?php endif; ?>
|
||||||
|
<ul class="event-hero-meta">
|
||||||
|
<?php if ($date_line !== ''): ?><li><i class="fas fa-calendar-alt"></i> <?= htmlspecialchars($date_line) ?></li><?php endif; ?>
|
||||||
|
<?php if ($event['location'] !== ''): ?><li><i class="fas fa-map-marker-alt"></i> <?= htmlspecialchars($event['location']) ?></li><?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
<?php if ($cta_label !== ''): ?>
|
||||||
|
<a href="<?= htmlspecialchars($cta_href, ENT_QUOTES) ?>" class="site-btn event-hero-cta"
|
||||||
|
style="background:<?= htmlspecialchars($accent, ENT_QUOTES) ?>"
|
||||||
|
<?= ($cta_href[0] ?? '') === '#' ? 'data-scroll' : '' ?>><?= htmlspecialchars($cta_label) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if (trim((string) $event['description']) !== ''): ?>
|
||||||
|
<section class="event-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-md-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<?php /* description is admin-authored HTML — sanitised allowlist HTML */ ?>
|
||||||
|
<?= clean_richtext($event['description']) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ((int) $event['show_lead_form'] === 1): ?>
|
||||||
|
<section class="event-leadform" id="inscricao">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-md-center">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="section-title text-center deco">
|
||||||
|
<span class="section-eyebrow">Participa</span>
|
||||||
|
<h2>Inscreve-te</h2>
|
||||||
|
<p class="section-subtitle">Deixa os teus dados e entramos em contacto.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['ok'])): ?>
|
||||||
|
<div class="event-alert event-alert--ok">Inscrição enviada com sucesso. Obrigado!</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($lead_errors as $e): ?>
|
||||||
|
<div class="event-alert event-alert--err"><?= htmlspecialchars($e) ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="<?= base_url('evento/' . rawurlencode($event['slug'])) ?>#inscricao" class="event-form">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<!-- honeypot: must stay empty -->
|
||||||
|
<input type="text" name="website" tabindex="-1" autocomplete="off" class="event-hp" aria-hidden="true">
|
||||||
|
<div class="event-form-grid">
|
||||||
|
<div class="event-field">
|
||||||
|
<label>Nome *</label>
|
||||||
|
<input type="text" name="lead_name" required maxlength="150" value="<?= htmlspecialchars($lead_old['name'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="event-field">
|
||||||
|
<label>Telefone</label>
|
||||||
|
<input type="tel" name="lead_phone" maxlength="40" value="<?= htmlspecialchars($lead_old['phone'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="event-field event-field--full">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" name="lead_email" maxlength="190" value="<?= htmlspecialchars($lead_old['email'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="event-field event-field--full">
|
||||||
|
<label>Mensagem</label>
|
||||||
|
<textarea name="lead_message" rows="3" maxlength="1000"><?= htmlspecialchars($lead_old['message'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="event-consent">
|
||||||
|
<input type="checkbox" name="lead_consent" value="1" required <?= !empty($lead_old['consent']) ? 'checked' : '' ?>>
|
||||||
|
<span>Autorizo o contacto e o tratamento dos meus dados para esta inscrição, nos termos da <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
|
||||||
|
</label>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button type="submit" class="site-btn" style="background:<?= htmlspecialchars($accent, ENT_QUOTES) ?>">Enviar inscrição</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
echo view('footer');
|
||||||
@@ -10,8 +10,10 @@ $page_meta = [
|
|||||||
'privacidade' => ['Política de Privacidade | FitMe 3070',
|
'privacidade' => ['Política de Privacidade | FitMe 3070',
|
||||||
'Política de privacidade do FitMe 3070 — como recolhemos, usamos e protegemos os teus dados pessoais.'],
|
'Política de privacidade do FitMe 3070 — como recolhemos, usamos e protegemos os teus dados pessoais.'],
|
||||||
];
|
];
|
||||||
[$page_title, $page_desc] = $page_meta[$page] ?? $page_meta[''];
|
// A dynamic page (e.g. an event landing page) can set its own title/description
|
||||||
$canonical = base_url($page !== '' ? 'home/' . $page : '');
|
// and canonical via these globals before requiring _open.php.
|
||||||
|
[$page_title, $page_desc] = $GLOBALS['_page_meta_override'] ?? $page_meta[$page] ?? $page_meta[''];
|
||||||
|
$canonical = $GLOBALS['_canonical_override'] ?? base_url($page !== '' ? 'home/' . $page : '');
|
||||||
?><!doctype html>
|
?><!doctype html>
|
||||||
<html lang="pt">
|
<html lang="pt">
|
||||||
<head>
|
<head>
|
||||||
@@ -54,14 +56,16 @@ $canonical = base_url($page !== '' ? 'home/' . $page : '');
|
|||||||
|
|
||||||
<!-- Open Graph / link previews -->
|
<!-- Open Graph / link previews -->
|
||||||
<meta property="og:url" content="<?= $canonical ?>">
|
<meta property="og:url" content="<?= $canonical ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="<?= htmlspecialchars($GLOBALS['_og_type_override'] ?? 'website') ?>">
|
||||||
<meta property="og:site_name" content="FitMe 3070">
|
<meta property="og:site_name" content="FitMe 3070">
|
||||||
<meta property="og:locale" content="pt_PT">
|
<meta property="og:locale" content="pt_PT">
|
||||||
<meta property="og:title" content="<?= htmlspecialchars($page_title) ?>">
|
<meta property="og:title" content="<?= htmlspecialchars($page_title) ?>">
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($page_desc) ?>">
|
<meta property="og:description" content="<?= htmlspecialchars($page_desc) ?>">
|
||||||
<meta property="og:image" content="<?= base_url('site/assets/img/og-image.jpg') ?>">
|
<meta property="og:image" content="<?= htmlspecialchars($GLOBALS['_og_image_override'] ?? base_url('site/assets/img/og-image.jpg')) ?>">
|
||||||
|
<?php if (empty($GLOBALS['_og_image_override'])): /* default banner is a known 1200×630; a per-page image isn't, so skip the dimension hints */ ?>
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630">
|
||||||
|
<?php endif; ?>
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
<!-- Structured data -->
|
<!-- Structured data -->
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="map" id="map">
|
<div class="map" id="map">
|
||||||
<?= isset($info['google_maps']) ? $info['google_maps'] : ''; ?>
|
<?= clean_map_embed($info['google_maps'] ?? '') ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +53,11 @@ else
|
|||||||
<!-- Smooth scroll + scroll-spy for one-page layout -->
|
<!-- Smooth scroll + scroll-spy for one-page layout -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
// This partial is printed by _open.php BEFORE the page sections (#equipa, …)
|
||||||
|
// exist in the DOM, so binding must wait for DOMContentLoaded — otherwise
|
||||||
|
// querySelector('#equipa') returns null, `sections` stays empty, and neither
|
||||||
|
// the smooth-scroll nor the scroll-spy (active nav link) ever bind.
|
||||||
|
function init() {
|
||||||
/* ── Eased smooth-scroll helper ──────────────────────────────────────── */
|
/* ── Eased smooth-scroll helper ──────────────────────────────────────── */
|
||||||
function smoothScrollTo(targetEl, duration) {
|
function smoothScrollTo(targetEl, duration) {
|
||||||
var start = window.scrollY;
|
var start = window.scrollY;
|
||||||
@@ -149,7 +154,15 @@ else
|
|||||||
// CrossFit page the list is empty and the PHP-set active pill must stay.
|
// CrossFit page the list is empty and the PHP-set active pill must stay.
|
||||||
if (sections.length > 0) {
|
if (sections.length > 0) {
|
||||||
window.addEventListener('scroll', updateActive, { passive: true });
|
window.addEventListener('scroll', updateActive, { passive: true });
|
||||||
|
window.addEventListener('resize', updateActive, { passive: true });
|
||||||
updateActive();
|
updateActive();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@@ -45,3 +45,10 @@ if ($ld_hours) $ld['openingHoursSpecification'] = $ld_hours;
|
|||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
<?= json_encode($ld, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?>
|
<?= json_encode($ld, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?>
|
||||||
</script>
|
</script>
|
||||||
|
<?php /* A dynamic page (e.g. an event landing page) can attach its own JSON-LD
|
||||||
|
— such as an Event schema — via this global, set before _open.php runs. */ ?>
|
||||||
|
<?php if (!empty($GLOBALS['_jsonld_extra'])): ?>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
<?= json_encode($GLOBALS['_jsonld_extra'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?>
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
+24
-3
@@ -10,6 +10,7 @@ $team = site_team($pdo);
|
|||||||
$modalidades = site_modalidades($pdo);
|
$modalidades = site_modalidades($pdo);
|
||||||
$horario = site_horario($pdo);
|
$horario = site_horario($pdo);
|
||||||
$whours = site_work_hours($pdo);
|
$whours = site_work_hours($pdo);
|
||||||
|
$events = site_active_events($pdo); // time-limited events, banner below the hero
|
||||||
|
|
||||||
// ── Hero section ────────────────────────────────────────────────────────────────
|
// ── Hero section ────────────────────────────────────────────────────────────────
|
||||||
?>
|
?>
|
||||||
@@ -27,6 +28,26 @@ $whours = site_work_hours($pdo);
|
|||||||
<section id="home" class="hero-section set-bg fade-in" style="background-image:url('<?= base_url('site/assets/img/home.jpg') ?>')">
|
<section id="home" class="hero-section set-bg fade-in" style="background-image:url('<?= base_url('site/assets/img/home.jpg') ?>')">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero-text">
|
<div class="hero-text">
|
||||||
|
<?php /* Eventos a decorrer — destacados no topo do header. Cada um liga
|
||||||
|
à sua landing page em /evento/<slug>. */ ?>
|
||||||
|
<?php if ($events): ?>
|
||||||
|
<div class="hero-events">
|
||||||
|
<?php foreach ($events as $ev):
|
||||||
|
$ac = $ev['cor'] !== '' ? $ev['cor'] : '#C2D500';
|
||||||
|
$ev_img = $ev['hero_image'] !== '' ? base_url('uploads/events/' . rawurlencode($ev['hero_image'])) : '';
|
||||||
|
?>
|
||||||
|
<a class="hero-event<?= $ev_img ? ' has-img' : '' ?>" href="<?= base_url('evento/' . rawurlencode($ev['slug'])) ?>"
|
||||||
|
style="--ev-accent:<?= htmlspecialchars($ac, ENT_QUOTES) ?><?= $ev_img ? ';--ev-img:url(\'' . htmlspecialchars($ev_img, ENT_QUOTES) . '\')' : '' ?>">
|
||||||
|
<span class="hero-event-badge"><span class="event-live-dot"></span><i class="fas fa-bolt"></i> Evento</span>
|
||||||
|
<span class="hero-event-text">
|
||||||
|
<strong class="hero-event-title"><?= htmlspecialchars($ev['title']) ?></strong>
|
||||||
|
<?php if ($ev['summary'] !== ''): ?><span class="hero-event-sub"><?= htmlspecialchars($ev['summary']) ?></span><?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span class="hero-event-go">Saber mais <i class="fas fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<span class="hero-brand">FitMe 3070</span>
|
<span class="hero-brand">FitMe 3070</span>
|
||||||
<h1>Ginásio, Box, Aulas<br><span>& Acompanhamento</span></h1>
|
<h1>Ginásio, Box, Aulas<br><span>& Acompanhamento</span></h1>
|
||||||
<div class="hero-cta-row">
|
<div class="hero-cta-row">
|
||||||
@@ -139,8 +160,8 @@ if ($modalidades):
|
|||||||
</div>
|
</div>
|
||||||
<h4><?= htmlspecialchars((string) $d['nome']) ?></h4>
|
<h4><?= htmlspecialchars((string) $d['nome']) ?></h4>
|
||||||
<?php if ($desc !== ''): ?>
|
<?php if ($desc !== ''): ?>
|
||||||
<?php /* descricao is admin-authored rich text — render its HTML, don't escape it */ ?>
|
<?php /* descricao is admin-authored rich text — sanitised allowlist HTML */ ?>
|
||||||
<div class="mod-tooltip" role="tooltip"><span><?= $desc ?></span></div>
|
<div class="mod-tooltip" role="tooltip"><span><?= clean_richtext($desc) ?></span></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -443,7 +464,7 @@ $session = session();
|
|||||||
if (strpos($maps_embed, 'loading=') === false) {
|
if (strpos($maps_embed, 'loading=') === false) {
|
||||||
$maps_embed = str_replace('<iframe ', '<iframe loading="lazy" ', $maps_embed);
|
$maps_embed = str_replace('<iframe ', '<iframe loading="lazy" ', $maps_embed);
|
||||||
}
|
}
|
||||||
echo $maps_embed;
|
echo clean_map_embed($maps_embed);
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../config/db.php';
|
require __DIR__ . '/../../config/db.php';
|
||||||
require __DIR__ . '/../../config/functions.php';
|
require __DIR__ . '/../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Counts for the dashboard widgets.
|
// Counts for the dashboard widgets.
|
||||||
$counts = [
|
$counts = [
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
// Shared event form fields, rendered inside the <form> of both create.php and
|
||||||
|
// edit.php. Expects:
|
||||||
|
// $data : associative array with every events column (see events_defaults())
|
||||||
|
// $item : the existing row on edit (for the current hero preview), or null
|
||||||
|
$item = $item ?? null;
|
||||||
|
// datetime-local inputs want "YYYY-MM-DDTHH:MM"; DB gives "YYYY-MM-DD HH:MM:SS".
|
||||||
|
$dt = static function ($v): string {
|
||||||
|
$v = trim((string) $v);
|
||||||
|
if ($v === '' || $v === '0000-00-00 00:00:00') return '';
|
||||||
|
return substr(str_replace(' ', 'T', $v), 0, 16);
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Título <span class="text-danger">*</span></label>
|
||||||
|
<input class="form-control" name="title" required maxlength="180" value="<?= htmlspecialchars($data['title'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Cor de destaque</label>
|
||||||
|
<input type="color" class="form-control form-control-color" name="cor" value="<?= htmlspecialchars($data['cor'] ?? '#C2D500') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Subtítulo</label>
|
||||||
|
<input class="form-control" name="subtitle" maxlength="255" value="<?= htmlspecialchars($data['subtitle'] ?? '') ?>">
|
||||||
|
<div class="form-text">Frase curta mostrada por baixo do título no topo da página.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Resumo</label>
|
||||||
|
<textarea class="form-control" name="summary" rows="2" maxlength="400"><?= htmlspecialchars($data['summary'] ?? '') ?></textarea>
|
||||||
|
<div class="form-text">Texto curto usado no banner da página inicial.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Descrição (conteúdo da página)</label>
|
||||||
|
<textarea class="form-control js-wysiwyg" data-wysiwyg-height="320" name="description" rows="8"><?= htmlspecialchars($data['description'] ?? '') ?></textarea>
|
||||||
|
<div class="form-text">Editor de texto formatado — é o corpo da landing page.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Imagem de destaque (hero)</label>
|
||||||
|
<input type="file" class="form-control" name="hero_file" accept=".png,.jpg,.jpeg,.gif,.webp">
|
||||||
|
<div class="form-text">Fundo do topo da landing page. Máx. 6 MB.</div>
|
||||||
|
<?php if ($item && !empty($item['hero_image'])): ?>
|
||||||
|
<div class="mt-2 d-flex align-items-center gap-2">
|
||||||
|
<img src="<?= base_url('uploads/events/' . rawurlencode($item['hero_image'])) ?>" alt="" style="height:48px;border-radius:4px;">
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input" type="checkbox" name="remove_hero" id="remove_hero" value="1">
|
||||||
|
<label class="form-check-label small" for="remove_hero">Remover imagem atual</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Local</label>
|
||||||
|
<input class="form-control" name="location" maxlength="200" value="<?= htmlspecialchars($data['location'] ?? '') ?>">
|
||||||
|
<div class="form-text">Onde decorre o evento (opcional).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12"><hr class="my-1"><span class="small text-uppercase text-muted fw-semibold">Quando decorre o evento</span></div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Início do evento</label>
|
||||||
|
<input type="datetime-local" class="form-control" name="event_starts_at" value="<?= htmlspecialchars($dt($data['event_starts_at'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Fim do evento</label>
|
||||||
|
<input type="datetime-local" class="form-control" name="event_ends_at" value="<?= htmlspecialchars($dt($data['event_ends_at'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12"><hr class="my-1"><span class="small text-uppercase text-muted fw-semibold">Janela de publicação no site</span></div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Publicar a partir de <span class="text-danger">*</span></label>
|
||||||
|
<input type="datetime-local" class="form-control" name="publish_from" required value="<?= htmlspecialchars($dt($data['publish_from'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Publicar até <span class="text-danger">*</span></label>
|
||||||
|
<input type="datetime-local" class="form-control" name="publish_until" required value="<?= htmlspecialchars($dt($data['publish_until'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_active" id="is_active" value="1" <?= (int) ($data['is_active'] ?? 1) === 1 ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="is_active">Ativo (interruptor manual — só aparece dentro da janela acima)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12"><hr class="my-1"><span class="small text-uppercase text-muted fw-semibold">Chamada para ação & inscrições</span></div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Texto do botão</label>
|
||||||
|
<input class="form-control" name="cta_label" maxlength="80" placeholder="ex: Inscreve-te já!" value="<?= htmlspecialchars($data['cta_label'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Link do botão</label>
|
||||||
|
<input class="form-control" name="cta_url" maxlength="255" placeholder="(vazio = ir para o formulário abaixo)" value="<?= htmlspecialchars($data['cta_url'] ?? '') ?>">
|
||||||
|
<div class="form-text">URL externo ou âncora. Em branco, o botão desce até ao formulário de inscrição.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="show_lead_form" id="show_lead_form" value="1" <?= (int) ($data['show_lead_form'] ?? 1) === 1 ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="show_lead_form">Mostrar formulário de inscrição na landing page</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../config/db.php';
|
||||||
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
|
$user = require_admin();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$data = events_defaults();
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
csrf_check();
|
||||||
|
$data = [
|
||||||
|
'title' => trim($_POST['title'] ?? ''),
|
||||||
|
'slug' => trim($_POST['slug'] ?? ''),
|
||||||
|
'subtitle' => trim($_POST['subtitle'] ?? ''),
|
||||||
|
'summary' => trim($_POST['summary'] ?? ''),
|
||||||
|
'description' => trim($_POST['description'] ?? ''),
|
||||||
|
'cor' => trim($_POST['cor'] ?? '#C2D500'),
|
||||||
|
'location' => trim($_POST['location'] ?? ''),
|
||||||
|
'event_starts_at' => trim($_POST['event_starts_at'] ?? ''),
|
||||||
|
'event_ends_at' => trim($_POST['event_ends_at'] ?? ''),
|
||||||
|
'publish_from' => trim($_POST['publish_from'] ?? ''),
|
||||||
|
'publish_until' => trim($_POST['publish_until'] ?? ''),
|
||||||
|
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
||||||
|
'cta_label' => trim($_POST['cta_label'] ?? ''),
|
||||||
|
'cta_url' => trim($_POST['cta_url'] ?? ''),
|
||||||
|
'show_lead_form' => isset($_POST['show_lead_form']) ? 1 : 0,
|
||||||
|
'hero_image' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($data['title'] === '') $errors[] = 'O título é obrigatório.';
|
||||||
|
if ($data['publish_from'] === '') $errors[] = 'A data de início da publicação é obrigatória.';
|
||||||
|
if ($data['publish_until'] === '') $errors[] = 'A data de fim da publicação é obrigatória.';
|
||||||
|
if ($data['publish_from'] !== '' && $data['publish_until'] !== ''
|
||||||
|
&& strtotime($data['publish_until']) < strtotime($data['publish_from'])) {
|
||||||
|
$errors[] = 'A data de fim da publicação não pode ser anterior à de início.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$hero_err = null;
|
||||||
|
$uploaded = events_image_upload($_FILES['hero_file'] ?? [], $hero_err);
|
||||||
|
if ($hero_err) $errors[] = $hero_err;
|
||||||
|
elseif ($uploaded) $data['hero_image'] = $uploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$data['slug'] = events_make_slug($pdo, $data['slug'] !== '' ? $data['slug'] : $data['title']);
|
||||||
|
$id = db_events_create($pdo, $data);
|
||||||
|
session()->setFlashdata('success', 'Evento criado.');
|
||||||
|
header('Location: ' . base_url('views/admin/events/edit.php?id=' . $id));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$page_title = 'Adicionar Evento';
|
||||||
|
include __DIR__ . '/../../../includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="container" style="max-width:820px;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Adicionar Evento</h1>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="index.php"><i class="bi bi-arrow-left me-1"></i>Voltar</a>
|
||||||
|
</div>
|
||||||
|
<?php foreach ($errors as $e): ?><div class="alert alert-danger"><?= htmlspecialchars($e) ?></div><?php endforeach; ?>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="card shadow-sm"><div class="card-body">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<?php include __DIR__ . '/_form.php'; ?>
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="index.php">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</div></form>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/../../../includes/footer.php'; ?>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../config/db.php';
|
||||||
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
|
$user = require_admin();
|
||||||
|
|
||||||
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
|
|
||||||
|
if (isset($_GET['restore'])) {
|
||||||
|
csrf_check();
|
||||||
|
db_events_restore($pdo, $id);
|
||||||
|
session()->setFlashdata('success', 'Evento restaurado.');
|
||||||
|
header('Location: ' . base_url('views/admin/events/edit.php?id=' . $id));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = db_events_get($pdo, $id);
|
||||||
|
if (!$item) { header('Location: index.php'); exit; }
|
||||||
|
|
||||||
|
// Delete a single lead.
|
||||||
|
if (isset($_POST['delete_lead'])) {
|
||||||
|
csrf_check();
|
||||||
|
db_event_lead_delete($pdo, (int) $_POST['delete_lead'], $id);
|
||||||
|
session()->setFlashdata('success', 'Inscrição removida.');
|
||||||
|
header('Location: ' . base_url('views/admin/events/edit.php?id=' . $id . '#leads'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$data = $item;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_event'])) {
|
||||||
|
csrf_check();
|
||||||
|
$data = [
|
||||||
|
'title' => trim($_POST['title'] ?? ''),
|
||||||
|
'slug' => trim($_POST['slug'] ?? ''),
|
||||||
|
'subtitle' => trim($_POST['subtitle'] ?? ''),
|
||||||
|
'summary' => trim($_POST['summary'] ?? ''),
|
||||||
|
'description' => trim($_POST['description'] ?? ''),
|
||||||
|
'cor' => trim($_POST['cor'] ?? '#C2D500'),
|
||||||
|
'location' => trim($_POST['location'] ?? ''),
|
||||||
|
'event_starts_at' => trim($_POST['event_starts_at'] ?? ''),
|
||||||
|
'event_ends_at' => trim($_POST['event_ends_at'] ?? ''),
|
||||||
|
'publish_from' => trim($_POST['publish_from'] ?? ''),
|
||||||
|
'publish_until' => trim($_POST['publish_until'] ?? ''),
|
||||||
|
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
||||||
|
'cta_label' => trim($_POST['cta_label'] ?? ''),
|
||||||
|
'cta_url' => trim($_POST['cta_url'] ?? ''),
|
||||||
|
'show_lead_form' => isset($_POST['show_lead_form']) ? 1 : 0,
|
||||||
|
'hero_image' => $item['hero_image'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($data['title'] === '') $errors[] = 'O título é obrigatório.';
|
||||||
|
if ($data['publish_from'] === '') $errors[] = 'A data de início da publicação é obrigatória.';
|
||||||
|
if ($data['publish_until'] === '') $errors[] = 'A data de fim da publicação é obrigatória.';
|
||||||
|
if ($data['publish_from'] !== '' && $data['publish_until'] !== ''
|
||||||
|
&& strtotime($data['publish_until']) < strtotime($data['publish_from'])) {
|
||||||
|
$errors[] = 'A data de fim da publicação não pode ser anterior à de início.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero: a new upload replaces the current image; an explicit "remover" clears it.
|
||||||
|
$uploaded = null;
|
||||||
|
if (!$errors) {
|
||||||
|
$hero_err = null;
|
||||||
|
$uploaded = events_image_upload($_FILES['hero_file'] ?? [], $hero_err);
|
||||||
|
if ($hero_err) $errors[] = $hero_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
if ($uploaded) {
|
||||||
|
events_image_remove($item['hero_image']);
|
||||||
|
$data['hero_image'] = $uploaded;
|
||||||
|
} elseif (!empty($_POST['remove_hero'])) {
|
||||||
|
events_image_remove($item['hero_image']);
|
||||||
|
$data['hero_image'] = '';
|
||||||
|
}
|
||||||
|
// Slug: keep the field's value (auto-regenerate from title only if emptied).
|
||||||
|
$data['slug'] = events_make_slug($pdo, $data['slug'] !== '' ? $data['slug'] : $data['title'], $id);
|
||||||
|
|
||||||
|
db_events_update($pdo, $id, $data);
|
||||||
|
session()->setFlashdata('success', 'Evento atualizado.');
|
||||||
|
header('Location: ' . base_url('views/admin/events/edit.php?id=' . $id));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// On error keep the existing slug for the form.
|
||||||
|
$data['slug'] = $item['slug'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$leads = db_event_leads_list($pdo, $id);
|
||||||
|
$status = events_status($item);
|
||||||
|
$page_title = 'Editar Evento';
|
||||||
|
include __DIR__ . '/../../../includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="container" style="max-width:820px;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">Editar Evento</h1>
|
||||||
|
<div class="small text-muted">/evento/<?= htmlspecialchars($item['slug']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<?php if (events_is_live($item)): ?>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('evento/' . rawurlencode($item['slug'])) ?>" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right me-1"></i>Ver página</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="index.php"><i class="bi bi-arrow-left me-1"></i>Voltar</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($item['deleted_at']): ?>
|
||||||
|
<div class="alert alert-warning d-flex justify-content-between align-items-center">
|
||||||
|
<span>Este evento está apagado.</span>
|
||||||
|
<form method="POST" action="<?= base_url('views/admin/events/edit.php?id=' . $id . '&restore=1') ?>" class="mb-0">
|
||||||
|
<?= csrf_field() ?><button class="btn btn-sm btn-info">Restaurar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($errors as $e): ?><div class="alert alert-danger"><?= htmlspecialchars($e) ?></div><?php endforeach; ?>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="card shadow-sm mb-4"><div class="card-body">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="save_event" value="1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Slug (URL)</label>
|
||||||
|
<input class="form-control" name="slug" value="<?= htmlspecialchars($data['slug'] ?? '') ?>">
|
||||||
|
<div class="form-text">Deixe em branco para gerar a partir do título.</div>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/_form.php'; ?>
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Atualizar</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="index.php">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</div></form>
|
||||||
|
|
||||||
|
<!-- ── Inscrições (leads) ─────────────────────────────────────────────── -->
|
||||||
|
<div class="card shadow-sm" id="leads"><div class="card-body">
|
||||||
|
<h2 class="h5 mb-3">Inscrições <span class="badge bg-secondary"><?= count($leads) ?></span></h2>
|
||||||
|
<?php if (!$leads): ?>
|
||||||
|
<p class="text-muted mb-0">Ainda não há inscrições para este evento.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light"><tr>
|
||||||
|
<th>Data</th><th>Nome</th><th>Email</th><th>Telefone</th><th>Mensagem</th><th class="text-center">Consent.</th><th></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($leads as $lead): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="small text-nowrap"><?= htmlspecialchars(format_date_pt((string) $lead['created_at'])) ?></td>
|
||||||
|
<td><?= htmlspecialchars($lead['name']) ?></td>
|
||||||
|
<td class="small"><?php if ($lead['email']): ?><a href="mailto:<?= htmlspecialchars($lead['email']) ?>"><?= htmlspecialchars($lead['email']) ?></a><?php else: ?><span class="text-muted">—</span><?php endif; ?></td>
|
||||||
|
<td class="small"><?= $lead['phone'] !== '' ? htmlspecialchars($lead['phone']) : '<span class="text-muted">—</span>' ?></td>
|
||||||
|
<td class="small" style="max-width:240px;"><?= $lead['message'] !== '' ? nl2br(htmlspecialchars($lead['message'])) : '<span class="text-muted">—</span>' ?></td>
|
||||||
|
<td class="text-center"><?php if (!empty($lead['consent'])): ?><span class="badge bg-success" title="<?= htmlspecialchars($lead['ip'] ?? '') ?>"><i class="bi bi-check-lg"></i></span><?php else: ?><span class="badge bg-secondary">—</span><?php endif; ?></td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" class="mb-0" onsubmit="return confirm('Remover esta inscrição?')">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" name="delete_lead" value="<?= (int) $lead['lead_id'] ?>"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/../../../includes/footer.php'; ?>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../config/db.php';
|
||||||
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
|
$user = require_admin();
|
||||||
|
|
||||||
|
// Handle delete BEFORE any output
|
||||||
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
csrf_check();
|
||||||
|
db_events_delete($pdo, (int) $_GET['delete']);
|
||||||
|
session()->setFlashdata('success', 'Evento apagado.');
|
||||||
|
header('Location: ' . base_url('views/admin/events/index.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = db_events_list($pdo, true); // include soft-deleted so they can be restored
|
||||||
|
|
||||||
|
// Status badge styling + label (PT).
|
||||||
|
$badge = [
|
||||||
|
'active' => ['bg-success', 'Ativo'],
|
||||||
|
'scheduled' => ['bg-info text-dark', 'Agendado'],
|
||||||
|
'expired' => ['bg-secondary', 'Expirado'],
|
||||||
|
'inactive' => ['bg-warning text-dark', 'Inativo'],
|
||||||
|
'deleted' => ['bg-dark', 'Apagado'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$page_title = 'Eventos';
|
||||||
|
include __DIR__ . '/../../../includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Eventos</h1>
|
||||||
|
<a class="btn btn-primary" href="<?= base_url('views/admin/events/create.php') ?>"><i class="bi bi-plus-lg me-1"></i>Adicionar Evento</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm"><div class="card-body p-0">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light"><tr>
|
||||||
|
<th>Evento</th><th>Estado</th><th>Janela de publicação</th><th class="text-center">Leads</th><th style="width:160px">Ações</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (!$items): ?>
|
||||||
|
<tr><td colspan="5" class="text-center text-muted py-4">Nenhum evento. <a href="<?= base_url('views/admin/events/create.php') ?>">Criar o primeiro</a>.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($items as $item):
|
||||||
|
$status = events_status($item);
|
||||||
|
[$cls, $lbl] = $badge[$status] ?? ['bg-light text-dark', $status];
|
||||||
|
$leads = db_event_leads_count($pdo, (int) $item['e_id']);
|
||||||
|
?>
|
||||||
|
<tr<?= $item['deleted_at'] ? ' class="opacity-50"' : '' ?>>
|
||||||
|
<td>
|
||||||
|
<strong><?= htmlspecialchars($item['title']) ?></strong>
|
||||||
|
<?php if ($item['summary']): ?><div class="small text-muted text-truncate" style="max-width:340px;"><?= htmlspecialchars($item['summary']) ?></div><?php endif; ?>
|
||||||
|
<div class="small text-muted">/evento/<?= htmlspecialchars($item['slug']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge <?= $cls ?>"><?= $lbl ?></span></td>
|
||||||
|
<td class="small">
|
||||||
|
<?= htmlspecialchars(format_date_pt((string) $item['publish_from'])) ?>
|
||||||
|
→
|
||||||
|
<?= htmlspecialchars(format_date_pt((string) $item['publish_until'])) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center"><?= $leads > 0 ? '<span class="badge bg-primary">' . $leads . '</span>' : '<span class="text-muted">0</span>' ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($item['deleted_at']): ?>
|
||||||
|
<form method="POST" action="<?= base_url('views/admin/events/edit.php?id=' . $item['e_id'] . '&restore=1') ?>" class="d-inline">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button class="btn btn-sm btn-outline-info">Restaurar</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (events_is_live($item)): ?>
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('evento/' . rawurlencode($item['slug'])) ?>" target="_blank" rel="noopener" title="Ver página"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="edit.php?id=<?= $item['e_id'] ?>"><i class="bi bi-pencil"></i></a>
|
||||||
|
<a class="btn btn-sm btn-outline-danger" href="?delete=<?= $item['e_id'] ?>" onclick="return confirm('Apagar este evento?')"><i class="bi bi-trash"></i></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/../../../includes/footer.php';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$name = '';
|
$name = '';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if (!$id) { header('Location: index.php'); exit; }
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Handle delete BEFORE any output
|
// Handle delete BEFORE any output
|
||||||
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$dias = ['segunda'=>'Segunda','terça'=>'Terça','quarta'=>'Quarta','quinta'=>'Quinta','sexta'=>'Sexta','sábado'=>'Sábado','domingo'=>'Domingo'];
|
$dias = ['segunda'=>'Segunda','terça'=>'Terça','quarta'=>'Quarta','quinta'=>'Quinta','sexta'=>'Sexta','sábado'=>'Sábado','domingo'=>'Domingo'];
|
||||||
$salas = ['sala1'=>'Sala 1','sala2'=>'Sala 2','crossfit'=>'CrossFit',''=>'Outro'];
|
$salas = ['sala1'=>'Sala 1','sala2'=>'Sala 2','crossfit'=>'CrossFit',''=>'Outro'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$data = ['id_modalidade' => '', 'dia_semana' => '', 'hora_inicio' => '09:00', 'hora_fim' => '10:00', 'local' => 'sala1'];
|
$data = ['id_modalidade' => '', 'dia_semana' => '', 'hora_inicio' => '09:00', 'hora_fim' => '10:00', 'local' => 'sala1'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if (!$id) { header('Location: index.php'); exit; }
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Handle delete BEFORE any output
|
// Handle delete BEFORE any output
|
||||||
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
/* ── Background image upload (AJAX, JSON) ─────────────────────────────────────
|
/* ── Background image upload (AJAX, JSON) ─────────────────────────────────────
|
||||||
Mirrors the fotografia "Spots IG" generator: saves a reusable background under
|
Mirrors the fotografia "Spots IG" generator: saves a reusable background under
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$data = ['nome' => '', 'cor' => '#808080', 'icon' => '', 'descricao' => '', 'item_order' => 0];
|
$data = ['nome' => '', 'cor' => '#808080', 'icon' => '', 'descricao' => '', 'item_order' => 0];
|
||||||
@@ -57,7 +57,7 @@ include __DIR__ . '/../../../includes/header.php';
|
|||||||
<div class="col-md-3"><label class="form-label">Ordem</label>
|
<div class="col-md-3"><label class="form-label">Ordem</label>
|
||||||
<input type="number" class="form-control" name="item_order" value="<?= (int) $data['item_order'] ?>" min="0"></div>
|
<input type="number" class="form-control" name="item_order" value="<?= (int) $data['item_order'] ?>" min="0"></div>
|
||||||
<div class="col-12"><label class="form-label">Descrição</label>
|
<div class="col-12"><label class="form-label">Descrição</label>
|
||||||
<textarea class="form-control" name="descricao" rows="3"><?= htmlspecialchars($data['descricao']) ?></textarea></div>
|
<textarea class="form-control js-wysiwyg" data-wysiwyg-height="200" name="descricao" rows="3"><?= htmlspecialchars($data['descricao']) ?></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($icons): ?>
|
<?php if ($icons): ?>
|
||||||
<div class="mt-3"><label class="form-label small text-muted">Ícones disponíveis</label>
|
<div class="mt-3"><label class="form-label small text-muted">Ícones disponíveis</label>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if (!$id) { header('Location: index.php'); exit; }
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
@@ -89,7 +89,7 @@ include __DIR__ . '/../../../includes/header.php';
|
|||||||
<div class="col-md-3"><label class="form-label">Ordem</label>
|
<div class="col-md-3"><label class="form-label">Ordem</label>
|
||||||
<input type="number" class="form-control" name="item_order" value="<?= (int) ($data['item_order'] ?? 0) ?>" min="0"></div>
|
<input type="number" class="form-control" name="item_order" value="<?= (int) ($data['item_order'] ?? 0) ?>" min="0"></div>
|
||||||
<div class="col-12"><label class="form-label">Descrição</label>
|
<div class="col-12"><label class="form-label">Descrição</label>
|
||||||
<textarea class="form-control" name="descricao" rows="3"><?= htmlspecialchars($data['descricao'] ?? '') ?></textarea></div>
|
<textarea class="form-control js-wysiwyg" data-wysiwyg-height="200" name="descricao" rows="3"><?= htmlspecialchars($data['descricao'] ?? '') ?></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($icons): ?>
|
<?php if ($icons): ?>
|
||||||
<div class="mt-3"><label class="form-label small text-muted">Ícones disponíveis</label>
|
<div class="mt-3"><label class="form-label small text-muted">Ícones disponíveis</label>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Handle delete BEFORE any output
|
// Handle delete BEFORE any output
|
||||||
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$data = ['name' => '', 'birthday' => '', 'at' => '', 'status' => 'ativo', 'description' => ''];
|
$data = ['name' => '', 'birthday' => '', 'at' => '', 'status' => 'ativo', 'description' => ''];
|
||||||
@@ -71,7 +71,7 @@ include __DIR__ . '/../../../includes/header.php';
|
|||||||
<div class="col-md-4"><label class="form-label">Foto</label>
|
<div class="col-md-4"><label class="form-label">Foto</label>
|
||||||
<input type="file" class="form-control" name="photo" accept=".jpg,.jpeg,.png,.gif,.webp"></div>
|
<input type="file" class="form-control" name="photo" accept=".jpg,.jpeg,.png,.gif,.webp"></div>
|
||||||
<div class="col-12"><label class="form-label">Descrição</label>
|
<div class="col-12"><label class="form-label">Descrição</label>
|
||||||
<textarea class="form-control" name="description" rows="3"><?= htmlspecialchars($data['description']) ?></textarea></div>
|
<textarea class="form-control js-wysiwyg" data-wysiwyg-height="200" name="description" rows="3"><?= htmlspecialchars($data['description']) ?></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 d-flex gap-2">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if (!$id) { header('Location: index.php'); exit; }
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
@@ -93,7 +93,7 @@ include __DIR__ . '/../../../includes/header.php';
|
|||||||
<input type="file" class="form-control" name="photo" accept=".jpg,.jpeg,.png,.gif,.webp">
|
<input type="file" class="form-control" name="photo" accept=".jpg,.jpeg,.png,.gif,.webp">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12"><label class="form-label">Descrição</label>
|
<div class="col-12"><label class="form-label">Descrição</label>
|
||||||
<textarea class="form-control" name="description" rows="3"><?= htmlspecialchars($data['description'] ?? '') ?></textarea></div>
|
<textarea class="form-control js-wysiwyg" data-wysiwyg-height="200" name="description" rows="3"><?= htmlspecialchars($data['description'] ?? '') ?></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 d-flex gap-2">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Atualizar</button>
|
<button type="submit" class="btn btn-primary">Atualizar</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Handle delete BEFORE any output
|
// Handle delete BEFORE any output
|
||||||
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$data = ['week_day' => '', 'open' => '09:00', 'close' => '18:00'];
|
$data = ['week_day' => '', 'open' => '09:00', 'close' => '18:00'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if (!$id) { header('Location: index.php'); exit; }
|
if (!$id) { header('Location: index.php'); exit; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../../config/db.php';
|
require __DIR__ . '/../../../config/db.php';
|
||||||
require __DIR__ . '/../../../config/functions.php';
|
require __DIR__ . '/../../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
// Handle delete BEFORE any output
|
// Handle delete BEFORE any output
|
||||||
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
if (isset($_GET['delete']) && is_numeric($_GET['delete'])) {
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ $repo_root = dirname(__DIR__, 2);
|
|||||||
require_once $repo_root . '/config/db.php';
|
require_once $repo_root . '/config/db.php';
|
||||||
require_once $repo_root . '/config/functions.php';
|
require_once $repo_root . '/config/functions.php';
|
||||||
require_once $repo_root . '/config/functions_backup.php';
|
require_once $repo_root . '/config/functions_backup.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_sysadmin();
|
$user = require_sysadmin();
|
||||||
require $repo_root . '/shared/platform-tools/backups.php';
|
require $repo_root . '/shared/platform-tools/backups.php';
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
$repo_root = dirname(__DIR__, 2);
|
$repo_root = dirname(__DIR__, 2);
|
||||||
require_once $repo_root . '/config/db.php';
|
require_once $repo_root . '/config/db.php';
|
||||||
require_once $repo_root . '/config/functions.php';
|
require_once $repo_root . '/config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_sysadmin();
|
$user = require_sysadmin();
|
||||||
require $repo_root . '/shared/platform-tools/platform_update.php';
|
require $repo_root . '/shared/platform-tools/platform_update.php';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../config/db.php';
|
require __DIR__ . '/../../config/db.php';
|
||||||
require __DIR__ . '/../../config/functions.php';
|
require __DIR__ . '/../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$logo_dir = __DIR__ . '/../../assets/img/logo/';
|
$logo_dir = __DIR__ . '/../../assets/img/logo/';
|
||||||
$allowed_ext = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
|
$allowed_ext = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../config/db.php';
|
require __DIR__ . '/../../config/db.php';
|
||||||
require __DIR__ . '/../../config/functions.php';
|
require __DIR__ . '/../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"client"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_login();
|
$user = require_login();
|
||||||
|
|
||||||
include __DIR__ . '/../../includes/header.php';
|
include __DIR__ . '/../../includes/header.php';
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../config/db.php';
|
require __DIR__ . '/../../config/db.php';
|
||||||
require __DIR__ . '/../../config/functions.php';
|
require __DIR__ . '/../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"sysadmin"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_admin();
|
$user = require_admin();
|
||||||
|
|
||||||
$users = $pdo->query(
|
$users = $pdo->query(
|
||||||
"SELECT id, name, email, phone, role, is_active, andmore_id, last_login_at, last_login_ua
|
"SELECT id, name, email, phone, role, is_active, andmore_id, last_login_at, last_login_ua
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/../../config/db.php';
|
require __DIR__ . '/../../config/db.php';
|
||||||
require __DIR__ . '/../../config/functions.php';
|
require __DIR__ . '/../../config/functions.php';
|
||||||
$user = ["id"=>1,"name"=>"Teste","email"=>"teste@fitme3070.pt","role"=>"client"]; $_SESSION["user_id"] = 1; $_SESSION["user"] = $user; // require_login();
|
$user = require_login();
|
||||||
|
|
||||||
// Editable per-tenant fields only. Name/email are owned by Andmore ID and synced
|
// Editable per-tenant fields only. Name/email are owned by Andmore ID and synced
|
||||||
// on login by sso_resolve_session(); the password is managed centrally.
|
// on login by sso_resolve_session(); the password is managed centrally.
|
||||||
|
|||||||
Reference in new issue
Block a user