Files
fitme3070.pt/shared/platform-tools/social/functions_publications.php
T
andreandClaude Opus 5 4d40adbf64 Vendor platform-tools: everything this site runs is now in one repo
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>
2026-09-13 23:06:23 +01:00

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'] ?? []),
];
}