shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.
The submodule caused every deployment failure this site has had:
- .gitmodules named no `branch`, so the update fell back to
refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
"Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
revision in submodule path".
- Its URL was http://, which the origin answers with 523.
- This install is deployed by uploading files, and a submodule is invisible
to that. The tree moved forward while shared/platform-tools stayed on the
old pin, config/functions_secrets.php died on a missing compat_secrets.php,
and the whole site — public pages included — went down.
Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.
The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
6.0 KiB
PHP
118 lines
6.0 KiB
PHP
<?php
|
|
// ─── Publications engine (shared, per-platform) ──────────────────────────────
|
|
//
|
|
// Moved out of the hub (PLANO_SIMPLIFICACAO_HUB.md §4.3): each platform now
|
|
// generates its own social posts from LOCAL data, so there is no content pull and
|
|
// no media proxy — photos are same-origin in the app itself. Pure client-side
|
|
// render (social/engine.js → html2canvas → JPG), driven by the CFG this file
|
|
// assembles and social/page.js consumes.
|
|
//
|
|
// A platform mounts views/admin/publicacoes.php (thin stub) that builds the items
|
|
// (the generic envelope) from its own queries, sets the platform colour, and
|
|
// requires social/publications.php. This file ships the formats + template
|
|
// catalogue (was the hub's SOCIAL_FORMATS const + social_templates table) and the
|
|
// CFG builder, so the contract that page.js expects stays byte-identical.
|
|
|
|
const PUB_FORMATS = [
|
|
'1:1' => ['label' => '1:1 Quadrado', 'w' => 2160, 'h' => 2160, 'group' => 'digital'],
|
|
'4:5' => ['label' => '4:5 Retrato', 'w' => 2160, 'h' => 2700, 'group' => 'digital'],
|
|
'9:16' => ['label' => '9:16 Story', 'w' => 2160, 'h' => 3840, 'group' => 'digital'],
|
|
'16:9' => ['label' => '16:9 Horizontal', 'w' => 3840, 'h' => 2160, 'group' => 'digital'],
|
|
'card' => ['label' => 'Cartão de visita', 'w' => 1004, 'h' => 650, 'group' => 'paper'],
|
|
'a4' => ['label' => 'A4', 'w' => 2480, 'h' => 3508, 'group' => 'paper'],
|
|
'a3' => ['label' => 'A3', 'w' => 3508, 'h' => 4961, 'group' => 'paper'],
|
|
'a2' => ['label' => 'A2', 'w' => 4961, 'h' => 7016, 'group' => 'paper'],
|
|
];
|
|
|
|
const PUB_MAX_BACKGROUNDS = 20;
|
|
|
|
// Template catalogue (was the social_templates table). multi = several items per
|
|
// slide; maxItems keyed by format id with a "default" fallback.
|
|
function pub_templates(): array {
|
|
return [
|
|
['slug' => 'event-list', 'name' => 'Lista de eventos', 'multi' => true,
|
|
'maxItems' => ['default' => 4, '9:16' => 6, '4:5' => 5, '16:9' => 3, 'a4' => 8, 'a3' => 8, 'a2' => 8, 'card' => 2],
|
|
'itemTypes' => ['event', 'manual']],
|
|
['slug' => 'single-announcement', 'name' => 'Anúncio único', 'multi' => false,
|
|
'maxItems' => ['default' => 1], 'itemTypes' => ['event', 'manual']],
|
|
['slug' => 'photo-highlight', 'name' => 'Foto em destaque', 'multi' => false,
|
|
'maxItems' => ['default' => 1], 'itemTypes' => ['photo_session']],
|
|
['slug' => 'photo-grid', 'name' => 'Grelha de fotos', 'multi' => true,
|
|
'maxItems' => ['default' => 4, '9:16' => 4, '1:1' => 4], 'itemTypes' => ['photo_session']],
|
|
];
|
|
}
|
|
|
|
// Keep only the universal envelope fields the templates rely on.
|
|
function pub_normalize_items(array $items): array {
|
|
$out = [];
|
|
foreach ($items as $it) {
|
|
if (!is_array($it)) continue;
|
|
$out[] = [
|
|
'id' => (string) ($it['id'] ?? uniqid('it_')),
|
|
'type' => (string) ($it['type'] ?? 'event'),
|
|
'title' => (string) ($it['title'] ?? ''),
|
|
'subtitle' => (string) ($it['subtitle'] ?? ''),
|
|
'date_start' => (string) ($it['date_start'] ?? ''),
|
|
'date_end' => (string) ($it['date_end'] ?? ''),
|
|
'url' => (string) ($it['url'] ?? ''),
|
|
'category' => (string) ($it['category'] ?? ''),
|
|
'category_label' => (string) ($it['category_label'] ?? ''),
|
|
'fields' => is_array($it['fields'] ?? null) ? $it['fields'] : [],
|
|
];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// Lighten (+) / darken (-) a hex colour by a percentage. Mirrors branding_shade.
|
|
function pub_shade(string $hex, int $pct): string {
|
|
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $hex)) return $hex;
|
|
$ch = function (int $c) use ($pct): int {
|
|
$delta = ($pct / 100) * ($pct < 0 ? $c : 255 - $c);
|
|
return max(0, min(255, (int) round($c + $delta)));
|
|
};
|
|
return sprintf('#%02x%02x%02x',
|
|
$ch(hexdec(substr($hex, 1, 2))),
|
|
$ch(hexdec(substr($hex, 3, 2))),
|
|
$ch(hexdec(substr($hex, 5, 2)))
|
|
);
|
|
}
|
|
|
|
// Colour presets for the picker, derived from the platform's single primary
|
|
// colour. Same {gradients, solids} shape the old branding presets produced.
|
|
function pub_presets(string $primary): array {
|
|
$primary = preg_match('/^#[0-9a-fA-F]{6}$/', $primary) ? $primary : '#5A7A62';
|
|
return [
|
|
'gradients' => [
|
|
['name' => 'Primária escura', 'css' => "linear-gradient(160deg, #101312, " . pub_shade($primary, -35) . ")"],
|
|
['name' => 'Primária', 'css' => "linear-gradient(160deg, " . pub_shade($primary, -55) . ", {$primary})"],
|
|
['name' => 'Noite', 'css' => 'linear-gradient(160deg, #0a0a0a, #2a2a2a)'],
|
|
],
|
|
'solids' => [
|
|
['name' => 'Primária', 'css' => $primary],
|
|
['name' => 'Preto', 'css' => '#0A0A0A'],
|
|
['name' => 'Branco', 'css' => '#FFFFFF'],
|
|
],
|
|
];
|
|
}
|
|
|
|
// Assemble the CFG that social/page.js consumes. $opts:
|
|
// slug, primary (#hex), items[] (envelope), photos[] (photo cfg), logo,
|
|
// watermark, category_label, preselected[]
|
|
function pub_build_config(array $opts): array {
|
|
$primary = (string) ($opts['primary'] ?? '#5A7A62');
|
|
return [
|
|
'app' => (string) ($opts['slug'] ?? ''),
|
|
'primary' => $primary,
|
|
'csrf' => function_exists('csrf_token') ? csrf_token() : '',
|
|
'formats' => PUB_FORMATS,
|
|
'templates' => pub_templates(),
|
|
'presets' => pub_presets($primary),
|
|
'logo' => ((string) ($opts['logo'] ?? '')) ?: null,
|
|
'watermark' => ((string) ($opts['watermark'] ?? '')) ?: null,
|
|
'dynamic' => pub_normalize_items($opts['items'] ?? []),
|
|
'dynamicPhoto' => array_values($opts['photos'] ?? []),
|
|
'categoryLabel' => (string) ($opts['category_label'] ?? 'Categoria'),
|
|
'presel' => array_values($opts['preselected'] ?? []),
|
|
];
|
|
}
|