Files
MyKeyser/admin/event_imports.php
Ty Clifford 354bb4255d - Rebuilt the CLI importer to use the official Facebook Graph API instead of HTML scraping.
Main changes:
Replaced 
[scripts/facebook-events-import.php](/Users/tyemeclifford/Documents/GH/MyKeyser/scripts/facebook-events-import.php) 
with a Graph API client.
It now supports --page-id, --pages-file, --event-id, --events-file, and 
optional area discovery via --discover-pages --center=LAT,LNG 
--query=....
Uses FACEBOOK_GRAPH_ACCESS_TOKEN or --access-token.
Pulls Graph fields for title, description, start/end time, 
place/location, ticket URI, and cover.source for the main photo.
Keeps the existing staged review/import workflow in 
/admin/event_imports.php.

- Implemented event detail pages and clickable event cards.
What changed:
Added [event.php](/Users/tyemeclifford/Documents/GH/MyKeyser/event.php) 
for individual event pages.
Made event cards clickable from 
[events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/events.php), 
[index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/index.php), and 
legacy 
[2index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/2index.php).
Added large event photo display plus lightbox enlargement in 
[assets/css/style.css](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/css/style.css) 
and 
[assets/js/app.js](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/js/app.js).
Added eventUrl() and externalHref() helpers in 
[includes/functions.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/functions.php).
Added copy-event-link support on event detail pages.
Updated README to list the new route.
2026-06-19 18:07:45 -04:00

247 lines
14 KiB
PHP

<?php
require_once dirname(__DIR__).'/includes/boot.php';
require_once dirname(__DIR__).'/includes/facebook_event_importer.php';
requireAdmin();
$adminTitle = 'Event Imports';
function eventImportRowsFromPost(): array {
$rows = p('rows', []);
return is_array($rows) ? $rows : [];
}
function eventImportSelectedIds(): array {
$ids = p('ids', []);
if (!is_array($ids)) $ids = [];
return array_values(array_unique(array_filter(array_map('intval', $ids))));
}
function eventImportSavePostedRows(array $onlyIds = []): void {
$wanted = array_fill_keys($onlyIds, true);
foreach (eventImportRowsFromPost() as $id => $row) {
$id = (int)$id;
if ($id <= 0) continue;
if ($onlyIds && !isset($wanted[$id])) continue;
if (!is_array($row)) continue;
facebookEventSaveCandidateFields($id, $row);
}
}
$filter = gs('filter') ?: p('filter', 'staged');
if (!in_array($filter, ['staged', 'imported', 'skipped', 'all'], true)) $filter = 'staged';
if (isPost()) {
csrfCheck();
$eventStatus = in_array(p('event_status', 'pending'), ['pending', 'approved'], true) ? p('event_status', 'pending') : 'pending';
$allowDuplicate = p('allow_duplicates', '0') === '1';
if (($saveId = (int)p('save_id', 0)) > 0) {
eventImportSavePostedRows([$saveId]);
flash('Import candidate saved.', 'success');
go('/admin/event_imports.php?filter='.$filter);
}
if (($importId = (int)p('import_id', 0)) > 0) {
eventImportSavePostedRows([$importId]);
$result = facebookEventImportCandidate($importId, uid(), $eventStatus, $allowDuplicate);
flash($result['ok'] ? 'Event imported.'.(!empty($result['warning']) ? ' '.$result['warning'] : '') : $result['error'], $result['ok'] ? 'success' : 'error');
go('/admin/event_imports.php?filter='.$filter);
}
if (($skipId = (int)p('skip_id', 0)) > 0) {
qrun("UPDATE event_imports SET status='skipped',updated_at=datetime('now') WHERE id=? AND status!='imported'", [$skipId]);
flash('Import candidate skipped.', 'success');
go('/admin/event_imports.php?filter='.$filter);
}
if (($restoreId = (int)p('restore_id', 0)) > 0) {
qrun("UPDATE event_imports SET status='staged',updated_at=datetime('now') WHERE id=? AND status='skipped'", [$restoreId]);
flash('Import candidate restored.', 'success');
go('/admin/event_imports.php?filter=staged');
}
if (($deleteId = (int)p('delete_id', 0)) > 0) {
qrun("DELETE FROM event_imports WHERE id=? AND status!='imported'", [$deleteId]);
flash('Import candidate deleted.', 'success');
go('/admin/event_imports.php?filter='.$filter);
}
$act = p('_act', '');
if ($act === 'save_all') {
eventImportSavePostedRows();
flash('Import candidates saved.', 'success');
go('/admin/event_imports.php?filter='.$filter);
}
if ($act === 'bulk_import') {
$ids = eventImportSelectedIds();
eventImportSavePostedRows($ids);
$ok = 0; $errors = [];
foreach ($ids as $id) {
$result = facebookEventImportCandidate($id, uid(), $eventStatus, $allowDuplicate);
if ($result['ok']) $ok++;
else $errors[] = '#'.$id.': '.$result['error'];
}
if ($ok) flash($ok.' event'.($ok === 1 ? '' : 's').' imported.', 'success');
if ($errors) flash(implode(' ', $errors), 'error');
if (!$ids) flash('Select at least one staged import first.', 'warning');
go('/admin/event_imports.php?filter='.$filter);
}
if ($act === 'bulk_skip') {
$ids = eventImportSelectedIds();
foreach ($ids as $id) qrun("UPDATE event_imports SET status='skipped',updated_at=datetime('now') WHERE id=? AND status!='imported'", [$id]);
flash($ids ? count($ids).' import candidate'.(count($ids) === 1 ? '' : 's').' skipped.' : 'Select at least one staged import first.', $ids ? 'success' : 'warning');
go('/admin/event_imports.php?filter='.$filter);
}
if ($act === 'bulk_delete') {
$ids = eventImportSelectedIds();
foreach ($ids as $id) qrun("DELETE FROM event_imports WHERE id=? AND status!='imported'", [$id]);
flash($ids ? count($ids).' import candidate'.(count($ids) === 1 ? '' : 's').' deleted.' : 'Select at least one staged import first.', $ids ? 'success' : 'warning');
go('/admin/event_imports.php?filter='.$filter);
}
}
$where = match ($filter) {
'staged' => "WHERE status='staged'",
'imported' => "WHERE status='imported'",
'skipped' => "WHERE status='skipped'",
default => '',
};
$imports = qall("SELECT * FROM event_imports $where ORDER BY CASE status WHEN 'staged' THEN 0 WHEN 'skipped' THEN 1 ELSE 2 END, created_at DESC LIMIT 200");
$counts = [
'staged' => (int)qval("SELECT COUNT(*) FROM event_imports WHERE status='staged'"),
'imported' => (int)qval("SELECT COUNT(*) FROM event_imports WHERE status='imported'"),
'skipped' => (int)qval("SELECT COUNT(*) FROM event_imports WHERE status='skipped'"),
'all' => (int)qval("SELECT COUNT(*) FROM event_imports"),
];
include __DIR__.'/admin_header.php';
?>
<?php adminTitle('Facebook Event Imports', 'Review staged Facebook event candidates, correct any fields, then import selected rows into the normal My Keyser events workflow.') ?>
<div class="ibox" style="border-color:var(--gold-d)">
<div class="ibox-title">CLI STAGING</div>
<p style="color:var(--text-m);font-size:.9rem;line-height:1.65;margin-bottom:.8rem">
Stage Facebook event candidates through the official Graph API, then return here to verify titles, dates, locations, duplicates, and photos before publishing or sending them to pending review.
</p>
<code style="display:block;white-space:normal;background:var(--bg2);border:1px solid var(--bdr);border-radius:var(--r);padding:.8rem;color:var(--text-m)">FACEBOOK_GRAPH_ACCESS_TOKEN=... php scripts/facebook-events-import.php --area="Keyser, WV" --page-id=123456789 --limit=20</code>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem">
<div class="flex-row" style="gap:.4rem">
<?php foreach(['staged'=>'Staged','imported'=>'Imported','skipped'=>'Skipped','all'=>'All'] as $key => $label): ?>
<a href="/admin/event_imports.php?filter=<?=$key?>" class="btn btn-xs<?=$filter===$key?' btn-primary':' btn-outline'?>"><?=$label?> (<?=$counts[$key]?>)</a>
<?php endforeach; ?>
</div>
<a href="/admin/events.php" class="btn btn-xs btn-outline">Manage Events</a>
</div>
<?php if (!$imports): ?>
<div class="empty"><div class="empty-ico">+</div><h3>No event imports here</h3><p style="margin-top:.5rem">Run the Graph API CLI importer to stage Facebook event candidates for review.</p></div>
<?php else: ?>
<form method="POST">
<?=csrfField()?>
<input type="hidden" name="filter" value="<?=e($filter)?>">
<div class="ibox" style="position:sticky;top:76px;z-index:2">
<div class="flex-row" style="justify-content:space-between;align-items:flex-end">
<div class="flex-row">
<div class="fg" style="margin-bottom:0;min-width:180px">
<label class="flabel">IMPORT AS</label>
<select name="event_status" class="fselect">
<option value="pending">Pending review</option>
<option value="approved">Approved public event</option>
</select>
</div>
<label style="display:flex;align-items:center;gap:.45rem;color:var(--text-m);font-size:.86rem;margin-top:1.25rem">
<input type="checkbox" name="allow_duplicates" value="1"> Allow duplicate title/date
</label>
</div>
<div class="flex-row" style="gap:.4rem">
<button type="submit" name="_act" value="save_all" class="btn btn-xs btn-outline">Save Edits</button>
<button type="submit" name="_act" value="bulk_import" class="btn btn-xs btn-success">Import Selected</button>
<button type="submit" name="_act" value="bulk_skip" class="btn btn-xs btn-secondary">Skip Selected</button>
<button type="submit" name="_act" value="bulk_delete" class="btn btn-xs btn-danger" data-confirm="Delete selected import candidates?">Delete Selected</button>
</div>
</div>
</div>
<div style="display:grid;gap:1rem">
<?php foreach ($imports as $imp): ?>
<?php
$id = (int)$imp['id'];
$issues = facebookEventCandidateIssues($imp);
$duplicateId = facebookEventDuplicateId($imp);
$statusClass = ['staged' => 'badge-gold', 'imported' => 'badge-green', 'skipped' => 'badge-gray'][$imp['status']] ?? 'badge-gray';
$preview = $imp['image_path'] ?: $imp['image_url'];
$canEdit = $imp['status'] !== 'imported';
?>
<div class="ibox" style="margin-bottom:0">
<div class="event-import-grid">
<div>
<?php if ($imp['status'] === 'staged'): ?>
<label style="display:flex;align-items:center;gap:.45rem;color:var(--text-m);font-size:.86rem;margin-bottom:.55rem">
<input type="checkbox" name="ids[]" value="<?=$id?>"> Select
</label>
<?php endif; ?>
<div class="event-import-preview">
<?php if ($preview): ?>
<img src="<?=e($preview)?>" alt="<?=e($imp['title'] ?: 'Event image')?>">
<?php else: ?>
No image
<?php endif; ?>
</div>
<div class="flex-row" style="gap:.35rem;margin-top:.65rem">
<span class="badge <?=$statusClass?>"><?=strtoupper(e($imp['status']))?></span>
<?php if (!$issues && !$duplicateId && $imp['status'] === 'staged'): ?><span class="badge badge-green">READY</span><?php endif; ?>
<?php if ($issues): ?><span class="badge badge-red">NEEDS FIX</span><?php endif; ?>
<?php if ($duplicateId && $imp['status'] === 'staged'): ?><span class="badge badge-gold">DUPLICATE?</span><?php endif; ?>
</div>
<?php if ($imp['source_url']): ?><p style="font-size:.78rem;margin-top:.6rem"><a href="<?=e($imp['source_url'])?>" target="_blank">View source</a></p><?php endif; ?>
<?php if ($imp['imported_event_id']): ?><p style="font-size:.78rem;margin-top:.25rem"><a href="/admin/events.php?edit=<?=(int)$imp['imported_event_id']?>">Edit imported event #<?=(int)$imp['imported_event_id']?></a></p><?php endif; ?>
</div>
<div style="min-width:0">
<div class="form-row">
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="rows[<?=$id?>][title]" class="finput" value="<?=e($imp['title'])?>" <?=$canEdit?'':'disabled'?>></div>
<div class="fg"><label class="flabel">SOURCE AREA</label><input type="text" class="finput" value="<?=e($imp['source_area'])?>" disabled></div>
</div>
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="rows[<?=$id?>][description]" class="ftextarea" style="min-height:76px" <?=$canEdit?'':'disabled'?>><?=e($imp['description'])?></textarea></div>
<div class="form-row">
<div class="fg"><label class="flabel">VENUE</label><input type="text" name="rows[<?=$id?>][venue]" class="finput" value="<?=e($imp['venue'])?>" <?=$canEdit?'':'disabled'?>></div>
<div class="fg"><label class="flabel">ADDRESS</label><input type="text" name="rows[<?=$id?>][address]" class="finput" value="<?=e($imp['address'])?>" <?=$canEdit?'':'disabled'?>></div>
</div>
<div class="form-row">
<div class="fg"><label class="flabel">START DATE *</label><input type="date" name="rows[<?=$id?>][event_date]" class="finput" value="<?=e($imp['event_date'])?>" <?=$canEdit?'':'disabled'?>></div>
<div class="fg"><label class="flabel">START TIME</label><input type="text" name="rows[<?=$id?>][event_time]" class="finput" value="<?=e($imp['event_time'])?>" placeholder="e.g. 7:00 PM" <?=$canEdit?'':'disabled'?>></div>
</div>
<div class="form-row">
<div class="fg"><label class="flabel">END DATE</label><input type="date" name="rows[<?=$id?>][end_date]" class="finput" value="<?=e($imp['end_date'])?>" <?=$canEdit?'':'disabled'?>></div>
<div class="fg"><label class="flabel">END TIME</label><input type="text" name="rows[<?=$id?>][end_time]" class="finput" value="<?=e($imp['end_time'])?>" <?=$canEdit?'':'disabled'?>></div>
</div>
<div class="form-row">
<div class="fg"><label class="flabel">COST</label><input type="text" name="rows[<?=$id?>][cost]" class="finput" value="<?=e($imp['cost'] ?: 'Free')?>" <?=$canEdit?'':'disabled'?>></div>
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="rows[<?=$id?>][website]" class="finput" value="<?=e($imp['website'])?>" <?=$canEdit?'':'disabled'?>></div>
</div>
<input type="hidden" name="rows[<?=$id?>][image_path]" value="<?=e($imp['image_path'])?>">
<?php if ($issues || $duplicateId || $imp['status_note']): ?>
<div style="background:rgba(201,168,76,.08);border:1px solid rgba(201,168,76,.2);border-radius:var(--r);padding:.75rem;margin:.4rem 0 1rem;color:var(--text-m);font-size:.84rem;line-height:1.55">
<?php if ($issues): ?><div><strong>Needs review:</strong> <?=e(implode(', ', $issues))?></div><?php endif; ?>
<?php if ($duplicateId && $imp['status'] === 'staged'): ?><div><strong>Possible duplicate:</strong> existing event #<?=$duplicateId?> has the same title and start date.</div><?php endif; ?>
<?php if ($imp['status_note']): ?><div><strong>Note:</strong> <?=e($imp['status_note'])?></div><?php endif; ?>
</div>
<?php endif; ?>
<div class="flex-row" style="gap:.4rem">
<?php if ($canEdit): ?><button type="submit" name="save_id" value="<?=$id?>" class="btn btn-xs btn-outline">Save</button><?php endif; ?>
<?php if ($imp['status'] === 'staged'): ?><button type="submit" name="import_id" value="<?=$id?>" class="btn btn-xs btn-success">Import</button><button type="submit" name="skip_id" value="<?=$id?>" class="btn btn-xs btn-secondary">Skip</button><?php endif; ?>
<?php if ($imp['status'] === 'skipped'): ?><button type="submit" name="restore_id" value="<?=$id?>" class="btn btn-xs btn-primary">Restore</button><?php endif; ?>
<?php if ($imp['status'] !== 'imported'): ?><button type="submit" name="delete_id" value="<?=$id?>" class="btn btn-xs btn-danger" data-confirm="Delete this import candidate?">Delete</button><?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</form>
<?php endif; ?>
<?php include __DIR__.'/admin_footer.php'; ?>