feat(site): Comunidade section with Instagram posts the gym was tagged in

Per-gym Graph API /tags fetch with file cache and local thumbnails, section after the gallery, navbar link and admin page (Gestão > Site).
This commit is contained in:
andre committed 2026-10-26 14:19:43 +00:00
1 parent 6ac5213624
commit e804c52b4e
10 files changed
+491 -3

No files matched your search

+176
View File
@@ -0,0 +1,176 @@
<?php
require __DIR__ . '/../../../config/db.php';
require __DIR__ . '/../../../config/functions.php';
$user = require_admin();
// Account, token and cache are per gym: this page works on the gym being edited.
$igym = admin_gym($pdo);
instagram_use_gym($igym);
$gid = (int) $igym['id'];
$self = base_url('views/admin/instagram/index.php');
$accounts = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$uid = (int) ($user['id'] ?? 0) ?: null;
$action = (string) ($_POST['action'] ?? '');
if ($action === 'save') {
$before = instagram_config();
// Token: blank keeps the stored one; the "remover" box clears it.
$token = trim((string) ($_POST['instagram_token'] ?? ''));
if (!empty($_POST['clear_token'])) {
db_update_setting($pdo, 'instagram_token_' . $gid, '', $uid);
} elseif ($token !== '') {
db_update_setting($pdo, 'instagram_token_' . $gid, secret_encrypt($token), $uid);
}
$ig_id = preg_replace('/\D+/', '', (string) ($_POST['instagram_user_id'] ?? ''));
db_update_setting($pdo, 'instagram_user_id_' . $gid, $ig_id, $uid);
db_update_setting($pdo, 'instagram_max', (string) max(3, min(24, (int) ($_POST['max'] ?? 8))), $uid);
db_update_setting($pdo, 'instagram_cache_hours', (string) max(1, (int) ($_POST['cache_hours'] ?? 6)), $uid);
// A new account or token starts from scratch (setting() is cached per request, so compare with what was posted).
if (!empty($_POST['clear_token']) || ($token !== '' && $token !== $before['token']) || $ig_id !== $before['user_id']) {
instagram_cache_clear();
}
session()->setFlashdata('success', 'Definições guardadas.');
header('Location: ' . $self);
exit;
}
if ($action === 'refresh') {
$r = instagram_refresh();
session()->setFlashdata(!empty($r['error']) ? 'error' : 'success',
!empty($r['error']) ? 'Falhou: ' . $r['error'] : 'Publicações atualizadas (' . count($r['posts'] ?? []) . ' com imagem).');
header('Location: ' . $self);
exit;
}
if ($action === 'accounts') {
// Lists the Facebook Pages the token manages and the Instagram account linked to each.
$token = instagram_config()['token'];
if ($token === '') {
$accounts = ['error' => 'Guarda primeiro o token.', 'list' => []];
} else {
[$json, $err] = _instagram_request(INSTAGRAM_GRAPH . 'me/accounts?' . http_build_query([
'fields' => 'name,instagram_business_account{id,username}', 'limit' => 50, 'access_token' => $token,
]));
$list = [];
foreach ((array) ($json['data'] ?? []) as $pg) {
if (!empty($pg['instagram_business_account']['id'])) {
$list[] = ['page' => (string) ($pg['name'] ?? ''), 'id' => (string) $pg['instagram_business_account']['id'],
'username' => (string) ($pg['instagram_business_account']['username'] ?? '')];
}
}
$accounts = ['error' => $err, 'list' => $list];
}
}
}
$cfg = instagram_config();
$env_tok = (string) env('INSTAGRAM_TOKEN_' . $gid, '') !== '';
$cache = instagram_cache_read();
$preview = site_instagram();
$page_title = 'Comunidade (Instagram)';
include __DIR__ . '/../../../includes/header.php';
?>
<div class="container-fluid" style="max-width:900px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 mb-0">Comunidade (Instagram)</h1>
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars(gym_url($igym, '#comunidade')) ?>" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right me-1"></i>Ver no site</a>
</div>
<p class="text-muted small">A secção «Comunidade» (a seguir à galeria) mostra as últimas publicações em que <strong><?= htmlspecialchars($igym['name']) ?></strong> foi identificado no Instagram. Só aparecem publicações de contas públicas. As imagens ficam guardadas no site; se não houver nenhuma, a secção fica escondida. Liga ou desliga a secção em Ginásios → Secções.</p>
<div class="card shadow-sm mb-4"><div class="card-body">
<h2 class="h5 mb-3">Configuração</h2>
<form method="POST">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
<div class="row g-3">
<div class="col-md-7">
<label class="form-label" for="instagram_token">Token de acesso (Graph API)</label>
<?php if ($env_tok): ?>
<input class="form-control" id="instagram_token" disabled value="Definido no .env (INSTAGRAM_TOKEN_<?= $gid ?>)">
<?php else: ?>
<input class="form-control font-monospace" type="password" id="instagram_token" name="instagram_token" autocomplete="off"
placeholder="<?= $cfg['token'] !== '' ? 'Guardado — deixa vazio para manter' : 'EAA…' ?>">
<?php if ($cfg['token'] !== ''): ?>
<div class="form-check mt-1"><input class="form-check-input" type="checkbox" name="clear_token" id="clear_token" value="1">
<label class="form-check-label small" for="clear_token">Remover token guardado</label></div>
<?php endif; ?>
<?php if (!secrets_available()): ?><div class="form-text text-warning">Sem chave de encriptação no .env — o token fica guardado sem encriptação.</div><?php endif; ?>
<?php endif; ?>
<div class="form-text">Token de um utilizador do sistema (Meta Business) ou de longa duração, com <code>instagram_basic</code>, <code>instagram_manage_comments</code> e <code>pages_read_engagement</code>.</div>
</div>
<div class="col-md-5">
<label class="form-label" for="instagram_user_id">ID da conta Instagram</label>
<input class="form-control font-monospace" id="instagram_user_id" name="instagram_user_id" inputmode="numeric" value="<?= htmlspecialchars($cfg['user_id']) ?>" placeholder="1784…">
<div class="form-text">Conta profissional ligada a uma Página do Facebook. Não sabes? Usa «Encontrar» abaixo.</div>
</div>
<div class="col-md-6">
<label class="form-label" for="ig_max">Número de publicações</label>
<input type="number" class="form-control" id="ig_max" name="max" min="3" max="24" value="<?= $cfg['max'] ?>">
<div class="form-text">Igual para todos os ginásios.</div>
</div>
<div class="col-md-6">
<label class="form-label" for="ig_hours">Atualizar a cada (horas)</label>
<input type="number" class="form-control" id="ig_hours" name="cache_hours" min="1" value="<?= $cfg['cache_hours'] ?>">
<div class="form-text">Igual para todos os ginásios.</div>
</div>
</div>
<button class="btn btn-primary mt-3">Guardar</button>
</form>
</div></div>
<div class="card shadow-sm mb-4"><div class="card-body">
<h2 class="h5 mb-3">Encontrar o ID da conta</h2>
<p class="small text-muted">Lista as Páginas do Facebook a que o token guardado tem acesso e a conta Instagram ligada a cada uma.</p>
<form method="POST" class="mb-0">
<?= csrf_field() ?>
<input type="hidden" name="action" value="accounts">
<button class="btn btn-outline-primary btn-sm" <?= $cfg['token'] !== '' ? '' : 'disabled' ?>><i class="bi bi-search me-1"></i>Encontrar</button>
</form>
<?php if ($accounts !== null): ?>
<?php if ($accounts['error'] !== ''): ?><div class="alert alert-danger mt-3 mb-0"><?= htmlspecialchars($accounts['error']) ?></div>
<?php elseif (!$accounts['list']): ?><div class="alert alert-warning mt-3 mb-0">Nenhuma Página com conta Instagram profissional ligada.</div>
<?php else: ?>
<ul class="list-group mt-3">
<?php foreach ($accounts['list'] as $a): ?>
<li class="list-group-item d-flex justify-content-between align-items-center gap-2">
<div><strong>@<?= htmlspecialchars($a['username']) ?></strong> <span class="small text-muted">· Página <?= htmlspecialchars($a['page']) ?></span>
<div><code class="small"><?= htmlspecialchars($a['id']) ?></code></div></div>
<button type="button" class="btn btn-sm btn-outline-success text-nowrap" onclick="document.getElementById('instagram_user_id').value='<?= htmlspecialchars($a['id'], ENT_QUOTES) ?>';window.scrollTo(0,0);">Usar</button>
</li>
<?php endforeach; ?>
</ul>
<div class="form-text">Depois de «Usar», carrega em Guardar.</div>
<?php endif; ?>
<?php endif; ?>
</div></div>
<div class="card shadow-sm"><div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Estado</h2>
<form method="POST" class="mb-0"><?= csrf_field() ?><input type="hidden" name="action" value="refresh">
<button class="btn btn-sm btn-outline-primary" <?= instagram_configured() ? '' : 'disabled' ?>><i class="bi bi-arrow-clockwise me-1"></i>Atualizar agora</button></form>
</div>
<?php if (!instagram_configured()): ?>
<p class="text-muted mb-0">Por configurar — a secção não aparece no site.</p>
<?php else: ?>
<p class="small mb-2">Última atualização: <?= !empty($cache['fetched_at']) ? date('d/m/Y H:i', (int) $cache['fetched_at']) : '—' ?></p>
<?php if (!empty($cache['error'])): ?><div class="alert alert-danger small">Último erro: <?= htmlspecialchars($cache['error']) ?></div><?php endif; ?>
<?php if ($preview): ?>
<p class="small text-muted mb-2">No site aparecem <?= count($preview) ?>:</p>
<div class="row g-2">
<?php foreach ($preview as $p): ?>
<div class="col-4 col-md-3">
<a href="<?= htmlspecialchars($p['permalink']) ?>" target="_blank" rel="noopener" class="d-block text-decoration-none small">
<img src="<?= htmlspecialchars(base_url($p['image'])) ?>" alt="" class="img-thumbnail w-100" style="aspect-ratio:1;object-fit:cover;" loading="lazy">
<span class="d-block text-truncate">@<?= htmlspecialchars($p['username']) ?></span>
</a>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-muted small mb-0">Ainda sem publicações identificadas — a secção está escondida.</p>
<?php endif; ?>
<?php endif; ?>
</div></div>
</div>
<?php include __DIR__ . '/../../../includes/footer.php';