|
|
|
@@ -0,0 +1,684 @@
|
|
|
|
|
<?php
|
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
|
|
|
|
|
|
$all_ads = [];
|
|
|
|
|
if (file_exists(ADS_JSON_PATH)) {
|
|
|
|
|
$raw = json_decode(file_get_contents(ADS_JSON_PATH), true);
|
|
|
|
|
if (is_array($raw)) $all_ads = $raw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$placements = unserialize(ADS_PLACEMENTS);
|
|
|
|
|
$sizes = unserialize(ADS_SIZES);
|
|
|
|
|
$sizes_map = array_column($sizes, null, 'key');
|
|
|
|
|
?>
|
|
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title><?= htmlspecialchars(ADS_MANAGER_TITLE) ?> — <?= htmlspecialchars(ADS_SITE_NAME) ?></title>
|
|
|
|
|
<meta name="robots" content="noindex, nofollow">
|
|
|
|
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* ── Design tokens — gate.php exact match ─────────────────────────────── */
|
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
:root {
|
|
|
|
|
--bg: #09090f;
|
|
|
|
|
--surface: #0f0f1a;
|
|
|
|
|
--surface-2: #141425;
|
|
|
|
|
--border: #1e1e38;
|
|
|
|
|
--border-2: #2a2a48;
|
|
|
|
|
--text: #e8e8f8;
|
|
|
|
|
--text-2: #9898c0;
|
|
|
|
|
--text-3: #58587a;
|
|
|
|
|
--green: #00e87a;
|
|
|
|
|
--blue: #00c8ff;
|
|
|
|
|
--purple: #b060ff;
|
|
|
|
|
--red: #ff3355;
|
|
|
|
|
--orange: #ff8800;
|
|
|
|
|
--pink: #ff50a0;
|
|
|
|
|
--pat: #ff424d;
|
|
|
|
|
--pat-glow: rgba(255,66,77,.18);
|
|
|
|
|
--r: 6px;
|
|
|
|
|
}
|
|
|
|
|
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
|
|
|
|
|
body {
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
background-image:
|
|
|
|
|
radial-gradient(ellipse 70% 50% at 20% 10%, rgba(176,96,255,.05) 0%, transparent 60%),
|
|
|
|
|
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(0,200,255,.04) 0%, transparent 60%),
|
|
|
|
|
radial-gradient(ellipse 50% 60% at 50% 50%, rgba(255,66,77,.03) 0%, transparent 70%);
|
|
|
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
display: flex; flex-direction: column; align-items: center;
|
|
|
|
|
padding: 1.5rem 1rem 3rem;
|
|
|
|
|
}
|
|
|
|
|
body::after {
|
|
|
|
|
content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 999;
|
|
|
|
|
background: repeating-linear-gradient(0deg, transparent 0px, transparent 3px, rgba(0,0,0,.07) 3px, rgba(0,0,0,.07) 4px);
|
|
|
|
|
}
|
|
|
|
|
@keyframes rise { from{opacity:0;transform:translateY(14px) scale(.98)} to{opacity:1;transform:translateY(0) scale(1)} }
|
|
|
|
|
@keyframes shimmer { from{background-position:0% 50%} to{background-position:200% 50%} }
|
|
|
|
|
@keyframes spin { to{transform:rotate(360deg)} }
|
|
|
|
|
@keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-5px)} 75%{transform:translateX(5px)} }
|
|
|
|
|
@keyframes fade-in { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
|
|
|
|
|
|
|
|
|
|
.page { width:100%; max-width:860px; display:flex; flex-direction:column; gap:1.25rem; animation:rise .4s cubic-bezier(.22,.68,0,1.2) both; position:relative; z-index:1; }
|
|
|
|
|
|
|
|
|
|
/* ── Card ─────────────────────────────────────────────────────────────── */
|
|
|
|
|
.card {
|
|
|
|
|
background: rgba(15,15,26,.9); border:1px solid var(--border);
|
|
|
|
|
border-radius: calc(var(--r)+4px); overflow:hidden; position:relative;
|
|
|
|
|
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
|
|
|
|
box-shadow: 0 0 0 1px rgba(255,255,255,.03), 0 6px 40px rgba(0,0,0,.6);
|
|
|
|
|
}
|
|
|
|
|
.card::before {
|
|
|
|
|
content:''; position:absolute; top:0; left:0; right:0; height:2px; z-index:2;
|
|
|
|
|
background:linear-gradient(90deg,var(--pat),var(--purple),var(--blue),var(--green),var(--blue),var(--purple),var(--pat));
|
|
|
|
|
background-size:300% 100%; animation:shimmer 5s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
.card::after {
|
|
|
|
|
content:''; position:absolute; bottom:-1px; right:-1px; width:14px; height:14px;
|
|
|
|
|
border-bottom:2px solid var(--purple); border-right:2px solid var(--purple);
|
|
|
|
|
border-radius:0 0 calc(var(--r)+4px) 0; opacity:.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Topbar ───────────────────────────────────────────────────────────── */
|
|
|
|
|
.topbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem; padding:1rem 1.25rem; }
|
|
|
|
|
.topbar-brand { display:flex; flex-direction:column; gap:.15rem; }
|
|
|
|
|
.topbar-eyebrow { font-family:'Share Tech Mono',monospace; font-size:.6rem; letter-spacing:.2em; text-transform:uppercase; color:var(--green); text-shadow:0 0 10px rgba(0,232,122,.5); }
|
|
|
|
|
.topbar-eyebrow::before { content:'> '; opacity:.5; }
|
|
|
|
|
.topbar-title { font-size:1.05rem; font-weight:700; letter-spacing:-.01em; }
|
|
|
|
|
|
|
|
|
|
/* ── Auth bar ─────────────────────────────────────────────────────────── */
|
|
|
|
|
.auth-bar { padding:.75rem 1.25rem; display:flex; align-items:center; gap:.75rem; border-bottom:1px solid var(--border); flex-wrap:wrap; }
|
|
|
|
|
.auth-bar label { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.14em; text-transform:uppercase; color:var(--blue); white-space:nowrap; }
|
|
|
|
|
.auth-bar input { flex:1; min-width:180px; padding:.5rem .85rem; font-family:'Share Tech Mono',monospace; font-size:.82rem; color:var(--text); background:rgba(255,255,255,.03); border:1px solid var(--border-2); border-radius:var(--r); outline:none; transition:border-color .2s,box-shadow .2s; }
|
|
|
|
|
.auth-bar input:focus { border-color:var(--blue); box-shadow:0 0 0 3px rgba(0,200,255,.1); }
|
|
|
|
|
.key-badge { display:inline-flex; align-items:center; gap:.35rem; padding:.22rem .6rem; background:rgba(0,232,122,.07); border:1px solid rgba(0,232,122,.2); border-radius:99px; font-family:'Share Tech Mono',monospace; font-size:.6rem; color:var(--green); }
|
|
|
|
|
.key-dot { width:6px; height:6px; border-radius:50%; background:var(--green); box-shadow:0 0 5px var(--green); }
|
|
|
|
|
|
|
|
|
|
/* ── Section head ─────────────────────────────────────────────────────── */
|
|
|
|
|
.section-head { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem; padding:.85rem 1.25rem .7rem; border-bottom:1px solid var(--border); }
|
|
|
|
|
.section-title { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.2em; text-transform:uppercase; color:var(--green); text-shadow:0 0 10px rgba(0,232,122,.5); }
|
|
|
|
|
.section-title::before { content:'> '; opacity:.5; }
|
|
|
|
|
|
|
|
|
|
/* ── Form fields ──────────────────────────────────────────────────────── */
|
|
|
|
|
.form-wrap { padding:1.1rem 1.25rem 1.25rem; display:flex; flex-direction:column; gap:.85rem; }
|
|
|
|
|
.field { display:flex; flex-direction:column; gap:.4rem; }
|
|
|
|
|
.field label { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.14em; text-transform:uppercase; color:var(--blue); text-shadow:0 0 8px rgba(0,200,255,.35); }
|
|
|
|
|
.field input, .field select, .field textarea {
|
|
|
|
|
width:100%; padding:.68rem .9rem; font-family:'Share Tech Mono',monospace; font-size:.85rem;
|
|
|
|
|
color:var(--text); background:rgba(255,255,255,.03); border:1px solid var(--border-2);
|
|
|
|
|
border-radius:var(--r); outline:none; transition:border-color .2s,box-shadow .2s,background .2s;
|
|
|
|
|
}
|
|
|
|
|
.field select { cursor:pointer; }
|
|
|
|
|
.field input:focus,.field select:focus,.field textarea:focus { border-color:var(--blue); background:rgba(0,200,255,.04); box-shadow:0 0 0 3px rgba(0,200,255,.1); }
|
|
|
|
|
.field input::placeholder,.field textarea::placeholder { color:var(--text-3); }
|
|
|
|
|
.field-row { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
|
|
|
|
|
@media(max-width:520px){.field-row{grid-template-columns:1fr}}
|
|
|
|
|
.field-hint { font-family:'Share Tech Mono',monospace; font-size:.6rem; color:var(--text-3); }
|
|
|
|
|
|
|
|
|
|
/* ── Size entries ─────────────────────────────────────────────────────── */
|
|
|
|
|
.sizes-label { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.14em; text-transform:uppercase; color:var(--blue); }
|
|
|
|
|
.size-list { display:flex; flex-direction:column; gap:.55rem; }
|
|
|
|
|
.size-entry {
|
|
|
|
|
background:rgba(255,255,255,.025); border:1px solid var(--border-2);
|
|
|
|
|
border-radius:var(--r); padding:.75rem .9rem;
|
|
|
|
|
display:flex; flex-direction:column; gap:.55rem; position:relative;
|
|
|
|
|
}
|
|
|
|
|
.size-entry-head { display:flex; align-items:center; justify-content:space-between; gap:.5rem; }
|
|
|
|
|
.size-entry-label { font-family:'Share Tech Mono',monospace; font-size:.68rem; color:var(--green); letter-spacing:.06em; }
|
|
|
|
|
.size-entry-grid { display:grid; grid-template-columns:1fr 1fr; gap:.55rem; }
|
|
|
|
|
@media(max-width:520px){.size-entry-grid{grid-template-columns:1fr}}
|
|
|
|
|
.size-add-btn {
|
|
|
|
|
display:inline-flex; align-items:center; gap:.4rem; padding:.45rem .9rem;
|
|
|
|
|
border:1px dashed var(--border-2); border-radius:var(--r);
|
|
|
|
|
background:rgba(255,255,255,.02); font-family:'Share Tech Mono',monospace;
|
|
|
|
|
font-size:.65rem; color:var(--text-3); cursor:pointer;
|
|
|
|
|
transition:border-color .15s,color .15s;
|
|
|
|
|
}
|
|
|
|
|
.size-add-btn:hover { border-color:var(--green); color:var(--green); }
|
|
|
|
|
|
|
|
|
|
/* ── Buttons ──────────────────────────────────────────────────────────── */
|
|
|
|
|
.btn { display:inline-flex; align-items:center; justify-content:center; gap:.4rem; padding:.6rem 1.1rem; border-radius:var(--r); font-family:'Inter',sans-serif; font-size:.84rem; font-weight:600; border:none; cursor:pointer; text-decoration:none; white-space:nowrap; transition:transform .15s,filter .15s; min-height:36px; position:relative; overflow:hidden; }
|
|
|
|
|
.btn::before { content:''; position:absolute; inset:0; background:linear-gradient(180deg,rgba(255,255,255,.09) 0%,transparent 60%); pointer-events:none; }
|
|
|
|
|
.btn:hover { transform:translateY(-1px); filter:brightness(1.08); }
|
|
|
|
|
.btn:active { transform:scale(.97); }
|
|
|
|
|
.btn:disabled { opacity:.4; cursor:not-allowed; transform:none; filter:none; }
|
|
|
|
|
.btn-green { background:var(--green); color:#050510; box-shadow:0 4px 16px rgba(0,232,122,.25); }
|
|
|
|
|
.btn-blue { background:var(--blue); color:#050510; box-shadow:0 4px 14px rgba(0,200,255,.2); }
|
|
|
|
|
.btn-red { background:var(--red); color:#fff; box-shadow:0 4px 14px rgba(255,51,85,.25); }
|
|
|
|
|
.btn-orange { background:var(--orange); color:#050510; box-shadow:0 4px 14px rgba(255,136,0,.2); }
|
|
|
|
|
.btn-ghost { background:rgba(255,255,255,.04); color:var(--text-2); border:1px solid var(--border-2); }
|
|
|
|
|
.btn-ghost:hover { border-color:var(--green); color:var(--text); }
|
|
|
|
|
.btn-sm { padding:.3rem .7rem; font-size:.74rem; min-height:28px; }
|
|
|
|
|
.btn .spinner { width:12px; height:12px; border-radius:50%; border:2px solid rgba(0,0,0,.2); border-top-color:currentColor; animation:spin .6s linear infinite; }
|
|
|
|
|
|
|
|
|
|
/* ── Alerts ───────────────────────────────────────────────────────────── */
|
|
|
|
|
.alert { display:flex; align-items:flex-start; gap:.6rem; border-radius:var(--r); padding:.7rem 1rem; font-family:'Share Tech Mono',monospace; font-size:.78rem; line-height:1.55; }
|
|
|
|
|
.alert-error { background:rgba(255,51,85,.08); border:1px solid rgba(255,51,85,.3); color:var(--red); animation:shake .35s ease; }
|
|
|
|
|
.alert-success { background:rgba(0,232,122,.07); border:1px solid rgba(0,232,122,.25); color:var(--green); }
|
|
|
|
|
.alert-info { background:rgba(0,200,255,.07); border:1px solid rgba(0,200,255,.25); color:var(--blue); }
|
|
|
|
|
|
|
|
|
|
/* ── Ad list ──────────────────────────────────────────────────────────── */
|
|
|
|
|
#ad-list { list-style:none; }
|
|
|
|
|
.ad-item { border-bottom:1px solid var(--border); padding:1rem 1.25rem; display:flex; flex-direction:column; gap:.6rem; animation:fade-in .25s ease both; }
|
|
|
|
|
.ad-item:last-child { border-bottom:none; }
|
|
|
|
|
.ad-item-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem; flex-wrap:wrap; }
|
|
|
|
|
.ad-item-title { font-size:.9rem; font-weight:600; color:var(--text); }
|
|
|
|
|
.ad-item-meta { display:flex; align-items:center; gap:.45rem; flex-wrap:wrap; }
|
|
|
|
|
.ad-badge {
|
|
|
|
|
font-family:'Share Tech Mono',monospace; font-size:.58rem; letter-spacing:.08em; text-transform:uppercase;
|
|
|
|
|
padding:.1em .45em; border-radius:3px; border:1px solid; white-space:nowrap;
|
|
|
|
|
}
|
|
|
|
|
.ad-badge-placement { color:var(--blue); border-color:rgba(0,200,255,.3); background:rgba(0,200,255,.07); }
|
|
|
|
|
.ad-badge-active { color:var(--green); border-color:rgba(0,232,122,.3); background:rgba(0,232,122,.07); }
|
|
|
|
|
.ad-badge-inactive { color:var(--text-3); border-color:var(--border-2); background:rgba(255,255,255,.02); }
|
|
|
|
|
.ad-badge-sizes { color:var(--purple); border-color:rgba(176,96,255,.3); background:rgba(176,96,255,.07); }
|
|
|
|
|
.ad-item-actions { display:flex; align-items:center; gap:.35rem; }
|
|
|
|
|
|
|
|
|
|
/* size thumb previews in list */
|
|
|
|
|
.ad-size-previews { display:flex; flex-wrap:wrap; gap:.35rem; }
|
|
|
|
|
.ad-size-pill {
|
|
|
|
|
display:inline-flex; align-items:center; gap:.35rem;
|
|
|
|
|
font-family:'Share Tech Mono',monospace; font-size:.58rem; letter-spacing:.05em;
|
|
|
|
|
color:var(--text-3); border:1px solid var(--border-2); border-radius:3px;
|
|
|
|
|
padding:.1em .45em; background:rgba(255,255,255,.02);
|
|
|
|
|
}
|
|
|
|
|
.ad-size-pill .media-dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
|
|
|
|
|
.ad-size-pill .media-dot.img-dot { background:var(--blue); box-shadow:0 0 4px var(--blue); }
|
|
|
|
|
.ad-size-pill .media-dot.vid-dot { background:var(--orange); box-shadow:0 0 4px var(--orange); }
|
|
|
|
|
|
|
|
|
|
/* ── Edit form inline ─────────────────────────────────────────────────── */
|
|
|
|
|
.edit-form { display:none; flex-direction:column; gap:.75rem; padding-top:.75rem; border-top:1px solid var(--border); margin-top:.25rem; }
|
|
|
|
|
.edit-form.open { display:flex; animation:fade-in .2s ease; }
|
|
|
|
|
|
|
|
|
|
/* ── Feed status / loading ────────────────────────────────────────────── */
|
|
|
|
|
.feed-status { padding:2.5rem 1.25rem; font-family:'Share Tech Mono',monospace; font-size:.72rem; color:var(--text-3); text-align:center; letter-spacing:.06em; }
|
|
|
|
|
.feed-status::before { content:'// '; opacity:.4; }
|
|
|
|
|
.loading-row { display:flex; align-items:center; justify-content:center; gap:.6rem; padding:2rem; }
|
|
|
|
|
.big-spinner { width:20px; height:20px; border-radius:50%; border:2px solid var(--border-2); border-top-color:var(--green); animation:spin .7s linear infinite; }
|
|
|
|
|
.loading-row span { font-family:'Share Tech Mono',monospace; font-size:.7rem; color:var(--text-3); }
|
|
|
|
|
|
|
|
|
|
/* ── Toggle switch ────────────────────────────────────────────────────── */
|
|
|
|
|
.toggle-wrap { display:inline-flex; align-items:center; gap:.5rem; cursor:pointer; }
|
|
|
|
|
.toggle { position:relative; width:36px; height:20px; }
|
|
|
|
|
.toggle input { opacity:0; width:0; height:0; }
|
|
|
|
|
.toggle-track { position:absolute; inset:0; background:var(--border-2); border-radius:99px; transition:background .2s; }
|
|
|
|
|
.toggle input:checked + .toggle-track { background:var(--green); }
|
|
|
|
|
.toggle-thumb { position:absolute; width:14px; height:14px; border-radius:50%; background:#fff; top:3px; left:3px; transition:transform .2s; }
|
|
|
|
|
.toggle input:checked ~ .toggle-thumb { transform:translateX(16px); }
|
|
|
|
|
.toggle-label { font-family:'Share Tech Mono',monospace; font-size:.65rem; color:var(--text-3); letter-spacing:.06em; }
|
|
|
|
|
|
|
|
|
|
/* ── Section divider ──────────────────────────────────────────────────── */
|
|
|
|
|
.divider { display:flex; align-items:center; gap:.6rem; font-family:'Share Tech Mono',monospace; font-size:.58rem; letter-spacing:.14em; text-transform:uppercase; color:var(--text-3); margin:.25rem 0; }
|
|
|
|
|
.divider::before,.divider::after { content:''; flex:1; height:1px; background:var(--border); }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="page">
|
|
|
|
|
|
|
|
|
|
<!-- Topbar -->
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="topbar">
|
|
|
|
|
<div class="topbar-brand">
|
|
|
|
|
<span class="topbar-eyebrow">Ad Manager</span>
|
|
|
|
|
<span class="topbar-title"><?= htmlspecialchars(ADS_SITE_NAME) ?> — <?= htmlspecialchars(ADS_MANAGER_TITLE) ?></span>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="key-badge" id="key-indicator" style="display:none">
|
|
|
|
|
<span class="key-dot"></span>authenticated
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Auth key input -->
|
|
|
|
|
<div class="auth-bar">
|
|
|
|
|
<label for="admin-key">Admin Key</label>
|
|
|
|
|
<input type="password" id="admin-key" placeholder="Enter admin key…" autocomplete="current-password">
|
|
|
|
|
<button class="btn btn-green btn-sm" onclick="setKey()">
|
|
|
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
|
|
|
Unlock
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="auth-alert" style="padding:0 1.25rem .75rem"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create new ad -->
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="section-head">
|
|
|
|
|
<span class="section-title">New Ad</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-wrap" id="create-form">
|
|
|
|
|
<div id="create-alert"></div>
|
|
|
|
|
|
|
|
|
|
<div class="field-row">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label for="c-title">Ad Title / Label <span style="color:var(--red)">*</span></label>
|
|
|
|
|
<input type="text" id="c-title" placeholder="e.g. Stream sponsor banner">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label for="c-placement">Placement <span style="color:var(--red)">*</span></label>
|
|
|
|
|
<select id="c-placement">
|
|
|
|
|
<?php foreach ($placements as $val => $label): ?>
|
|
|
|
|
<option value="<?= htmlspecialchars($val) ?>"><?= htmlspecialchars($label) ?></option>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label for="c-click-url">Click-through URL</label>
|
|
|
|
|
<input type="url" id="c-click-url" placeholder="https://…">
|
|
|
|
|
<span class="field-hint">Where clicking the ad takes the visitor. Leave blank for no link.</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label for="c-note">Internal note</label>
|
|
|
|
|
<input type="text" id="c-note" placeholder="Optional — campaign name, dates, etc.">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="divider">Size variants</div>
|
|
|
|
|
<span class="field-hint" style="margin-top:-.4rem">Add one entry per screen size. The live page shows the best-fitting variant.</span>
|
|
|
|
|
|
|
|
|
|
<div class="sizes-label">Creative sizes</div>
|
|
|
|
|
<div class="size-list" id="c-size-list"></div>
|
|
|
|
|
<button class="size-add-btn" onclick="addSizeEntry('c-size-list')">
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
|
|
|
Add size variant
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap;padding-top:.25rem">
|
|
|
|
|
<button class="btn btn-green" id="create-btn" onclick="submitAd()">
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
|
|
|
Save ad
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="clearCreate()">Clear</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Ad list -->
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="section-head">
|
|
|
|
|
<span class="section-title">All Ads</span>
|
|
|
|
|
<span id="ad-count" style="font-family:'Share Tech Mono',monospace;font-size:.58rem;color:var(--text-3)">
|
|
|
|
|
<?= count($all_ads) ?> ad<?= count($all_ads) !== 1 ? 's' : '' ?>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<ul id="ad-list">
|
|
|
|
|
<?php if (empty($all_ads)): ?>
|
|
|
|
|
<li class="feed-status">No ads yet — create one above.</li>
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
<?php foreach ($all_ads as $ad): ?>
|
|
|
|
|
<li class="ad-item" id="ai-<?= htmlspecialchars($ad['id']) ?>">
|
|
|
|
|
<?php
|
|
|
|
|
$is_active = $ad['active'] ?? true;
|
|
|
|
|
$pl_label = $placements[$ad['placement'] ?? 'above'] ?? ($ad['placement'] ?? '');
|
|
|
|
|
$size_count = count($ad['sizes'] ?? []);
|
|
|
|
|
?>
|
|
|
|
|
<div class="ad-item-head">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="ad-item-title"><?= htmlspecialchars($ad['title'] ?? 'Untitled') ?></div>
|
|
|
|
|
<?php if (!empty($ad['note'])): ?>
|
|
|
|
|
<div style="font-family:'Share Tech Mono',monospace;font-size:.62rem;color:var(--text-3);margin-top:.2rem"><?= htmlspecialchars($ad['note']) ?></div>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ad-item-actions">
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="toggleEdit('<?= htmlspecialchars($ad['id']) ?>')">Edit</button>
|
|
|
|
|
<button class="btn btn-red btn-sm" onclick="deleteAd('<?= htmlspecialchars($ad['id']) ?>')">Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ad-item-meta">
|
|
|
|
|
<span class="ad-badge ad-badge-placement"><?= htmlspecialchars($pl_label) ?></span>
|
|
|
|
|
<span class="ad-badge <?= $is_active ? 'ad-badge-active' : 'ad-badge-inactive' ?>" id="badge-active-<?= htmlspecialchars($ad['id']) ?>">
|
|
|
|
|
<?= $is_active ? 'Active' : 'Paused' ?>
|
|
|
|
|
</span>
|
|
|
|
|
<?php if ($size_count > 0): ?>
|
|
|
|
|
<span class="ad-badge ad-badge-sizes"><?= $size_count ?> size<?= $size_count !== 1 ? 's' : '' ?></span>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
<label class="toggle-wrap" title="Toggle active">
|
|
|
|
|
<span class="toggle">
|
|
|
|
|
<input type="checkbox" <?= $is_active ? 'checked' : '' ?>
|
|
|
|
|
onchange="toggleActive('<?= htmlspecialchars($ad['id']) ?>', this.checked)">
|
|
|
|
|
<span class="toggle-track"></span>
|
|
|
|
|
<span class="toggle-thumb"></span>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="toggle-label">Active</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<?php if (!empty($ad['sizes'])): ?>
|
|
|
|
|
<div class="ad-size-previews">
|
|
|
|
|
<?php foreach ($ad['sizes'] as $s): ?>
|
|
|
|
|
<?php
|
|
|
|
|
$sk = $s['size_key'] ?? '';
|
|
|
|
|
$sm = $sizes_map[$sk] ?? null;
|
|
|
|
|
$sz_label = $sm ? $sm['label'] : $sk;
|
|
|
|
|
$sz_type = $s['media_type'] ?? 'image';
|
|
|
|
|
?>
|
|
|
|
|
<span class="ad-size-pill">
|
|
|
|
|
<span class="media-dot <?= $sz_type === 'image' ? 'img-dot' : 'vid-dot' ?>"></span>
|
|
|
|
|
<?= htmlspecialchars($sz_label) ?>
|
|
|
|
|
</span>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</div>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
<!-- Inline edit form -->
|
|
|
|
|
<div class="edit-form" id="ef-<?= htmlspecialchars($ad['id']) ?>">
|
|
|
|
|
<div class="field-row">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Title</label>
|
|
|
|
|
<input type="text" id="ef-title-<?= htmlspecialchars($ad['id']) ?>"
|
|
|
|
|
value="<?= htmlspecialchars($ad['title'] ?? '') ?>">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Placement</label>
|
|
|
|
|
<select id="ef-placement-<?= htmlspecialchars($ad['id']) ?>">
|
|
|
|
|
<?php foreach ($placements as $val => $label): ?>
|
|
|
|
|
<option value="<?= htmlspecialchars($val) ?>" <?= ($ad['placement'] ?? '') === $val ? 'selected' : '' ?>>
|
|
|
|
|
<?= htmlspecialchars($label) ?>
|
|
|
|
|
</option>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Click-through URL</label>
|
|
|
|
|
<input type="url" id="ef-click-url-<?= htmlspecialchars($ad['id']) ?>"
|
|
|
|
|
value="<?= htmlspecialchars($ad['click_url'] ?? '') ?>">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Internal note</label>
|
|
|
|
|
<input type="text" id="ef-note-<?= htmlspecialchars($ad['id']) ?>"
|
|
|
|
|
value="<?= htmlspecialchars($ad['note'] ?? '') ?>">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="divider">Size variants</div>
|
|
|
|
|
<div class="sizes-label">Creative sizes</div>
|
|
|
|
|
<div class="size-list" id="ef-size-list-<?= htmlspecialchars($ad['id']) ?>">
|
|
|
|
|
<?php foreach (($ad['sizes'] ?? []) as $si => $s): ?>
|
|
|
|
|
<?php $this_key = htmlspecialchars($ad['id']) . '-' . $si; ?>
|
|
|
|
|
<div class="size-entry" data-idx="<?= $si ?>">
|
|
|
|
|
<div class="size-entry-head">
|
|
|
|
|
<span class="size-entry-label">Variant <?= $si + 1 ?></span>
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="this.closest('.size-entry').remove()">✕ Remove</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="size-entry-grid">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Size preset</label>
|
|
|
|
|
<select class="size-key-sel">
|
|
|
|
|
<?php foreach ($sizes as $sz): ?>
|
|
|
|
|
<option value="<?= htmlspecialchars($sz['key']) ?>"
|
|
|
|
|
<?= ($s['size_key'] ?? '') === $sz['key'] ? 'selected' : '' ?>>
|
|
|
|
|
<?= htmlspecialchars($sz['label']) ?>
|
|
|
|
|
</option>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Media type</label>
|
|
|
|
|
<select class="size-mtype-sel">
|
|
|
|
|
<option value="image" <?= ($s['media_type'] ?? 'image') === 'image' ? 'selected' : '' ?>>Image</option>
|
|
|
|
|
<option value="video" <?= ($s['media_type'] ?? '') === 'video' ? 'selected' : '' ?>>Video</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Media URL (image or video)</label>
|
|
|
|
|
<input type="url" class="size-url-inp" placeholder="https://…"
|
|
|
|
|
value="<?= htmlspecialchars($s['media_url'] ?? '') ?>">
|
|
|
|
|
<span class="field-hint">Paste a full URL. For local files: /media/filename.jpg</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="size-entry-grid custom-dims" style="<?= ($s['size_key'] ?? '') !== 'custom' ? 'display:none' : '' ?>">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Custom width (px)</label>
|
|
|
|
|
<input type="number" class="size-cw-inp" placeholder="728"
|
|
|
|
|
value="<?= htmlspecialchars((string)($s['custom_w'] ?? '')) ?>">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Custom height (px)</label>
|
|
|
|
|
<input type="number" class="size-ch-inp" placeholder="90"
|
|
|
|
|
value="<?= htmlspecialchars((string)($s['custom_h'] ?? '')) ?>">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="size-add-btn" onclick="addSizeEntry('ef-size-list-<?= htmlspecialchars($ad['id']) ?>')">
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
|
|
|
Add size variant
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap;">
|
|
|
|
|
<button class="btn btn-blue btn-sm" onclick="saveEdit('<?= htmlspecialchars($ad['id']) ?>')">
|
|
|
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
|
|
|
Save changes
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="toggleEdit('<?= htmlspecialchars($ad['id']) ?>')">Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="ef-alert-<?= htmlspecialchars($ad['id']) ?>"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div><!-- /page -->
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
/* ── Ad size presets (from PHP config) ───────────────────────────────────── */
|
|
|
|
|
const SIZE_PRESETS = <?= json_encode($sizes) ?>;
|
|
|
|
|
const SAVE_URL = './save.php';
|
|
|
|
|
const STORE_KEY = 'tc_admanager_key';
|
|
|
|
|
|
|
|
|
|
let adminKey = '';
|
|
|
|
|
|
|
|
|
|
/* ── Helpers ────────────────────────────────────────────────────────────── */
|
|
|
|
|
function esc(s) {
|
|
|
|
|
return String(s??'').replace(/&/g,'&').replace(/</g,'<')
|
|
|
|
|
.replace(/>/g,'>').replace(/"/g,'"');
|
|
|
|
|
}
|
|
|
|
|
function showAlert(id, type, msg) {
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
if (!el) return;
|
|
|
|
|
el.innerHTML = `<div class="alert alert-${type}">${esc(msg)}</div>`;
|
|
|
|
|
setTimeout(() => el.innerHTML = '', 6000);
|
|
|
|
|
}
|
|
|
|
|
function setLoading(id, on) {
|
|
|
|
|
const b = document.getElementById(id); if (!b) return;
|
|
|
|
|
b.disabled = on;
|
|
|
|
|
const sp = b.querySelector('.spinner');
|
|
|
|
|
if (on && !sp) b.insertAdjacentHTML('afterbegin','<span class="spinner"></span>');
|
|
|
|
|
if (!on && sp) sp.remove();
|
|
|
|
|
}
|
|
|
|
|
async function apiFetch(qs, opts={}) {
|
|
|
|
|
const res = await fetch(SAVE_URL + (qs?'?'+qs:''), {
|
|
|
|
|
...opts, headers: {'Content-Type':'application/json','X-Ad-Key':adminKey,...(opts.headers||{})}
|
|
|
|
|
});
|
|
|
|
|
const j = await res.json();
|
|
|
|
|
if (!j.ok) throw new Error(j.error||'API error');
|
|
|
|
|
return j.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Auth ───────────────────────────────────────────────────────────────── */
|
|
|
|
|
function setKey() {
|
|
|
|
|
const k = document.getElementById('admin-key').value.trim();
|
|
|
|
|
if (!k) { showAlert('auth-alert','error','Enter the admin key.'); return; }
|
|
|
|
|
adminKey = k;
|
|
|
|
|
// Test with a harmless fetch (try to load list)
|
|
|
|
|
fetch(SAVE_URL + '?_test=1', {method:'DELETE', headers:{'X-Ad-Key':k}})
|
|
|
|
|
.then(r => r.json()).then(j => {
|
|
|
|
|
if (j.error && j.error.includes('id')) {
|
|
|
|
|
// Got a real API response → key accepted
|
|
|
|
|
sessionStorage.setItem(STORE_KEY, k);
|
|
|
|
|
document.getElementById('key-indicator').style.display = '';
|
|
|
|
|
showAlert('auth-alert','success','Authenticated successfully.');
|
|
|
|
|
} else if (!j.ok && j.error && j.error.includes('Invalid')) {
|
|
|
|
|
adminKey = '';
|
|
|
|
|
showAlert('auth-alert','error','Invalid key.');
|
|
|
|
|
} else {
|
|
|
|
|
sessionStorage.setItem(STORE_KEY, k);
|
|
|
|
|
document.getElementById('key-indicator').style.display = '';
|
|
|
|
|
showAlert('auth-alert','success','Authenticated.');
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => showAlert('auth-alert','error','Could not reach save.php.'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('admin-key').addEventListener('keydown', e => { if(e.key==='Enter') setKey(); });
|
|
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
const saved = sessionStorage.getItem(STORE_KEY);
|
|
|
|
|
if (saved) {
|
|
|
|
|
document.getElementById('admin-key').value = saved;
|
|
|
|
|
adminKey = saved;
|
|
|
|
|
document.getElementById('key-indicator').style.display = '';
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* ── Size entry builder ─────────────────────────────────────────────────── */
|
|
|
|
|
function addSizeEntry(listId) {
|
|
|
|
|
const list = document.getElementById(listId);
|
|
|
|
|
const idx = list.querySelectorAll('.size-entry').length;
|
|
|
|
|
const presetOpts = SIZE_PRESETS.map(s =>
|
|
|
|
|
`<option value="${esc(s.key)}">${esc(s.label)}</option>`).join('');
|
|
|
|
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'size-entry';
|
|
|
|
|
div.innerHTML = `
|
|
|
|
|
<div class="size-entry-head">
|
|
|
|
|
<span class="size-entry-label">Variant ${idx + 1}</span>
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="this.closest('.size-entry').remove()">✕ Remove</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="size-entry-grid">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Size preset</label>
|
|
|
|
|
<select class="size-key-sel" onchange="toggleCustomDims(this)">${presetOpts}</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Media type</label>
|
|
|
|
|
<select class="size-mtype-sel">
|
|
|
|
|
<option value="image">Image</option>
|
|
|
|
|
<option value="video">Video</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Media URL (image or video)</label>
|
|
|
|
|
<input type="url" class="size-url-inp" placeholder="https://… or /media/file.jpg">
|
|
|
|
|
<span class="field-hint">Full URL or server-relative path. Supports JPEG, PNG, GIF, WEBP, MP4, WEBM.</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="size-entry-grid custom-dims" style="display:none">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Custom width (px)</label>
|
|
|
|
|
<input type="number" class="size-cw-inp" placeholder="728">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Custom height (px)</label>
|
|
|
|
|
<input type="number" class="size-ch-inp" placeholder="90">
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleCustomDims(sel) {
|
|
|
|
|
const entry = sel.closest('.size-entry');
|
|
|
|
|
const dims = entry.querySelector('.custom-dims');
|
|
|
|
|
if (dims) dims.style.display = sel.value === 'custom' ? '' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectSizes(listId) {
|
|
|
|
|
const entries = document.querySelectorAll('#' + listId + ' .size-entry');
|
|
|
|
|
const sizes = [];
|
|
|
|
|
entries.forEach(e => {
|
|
|
|
|
const key = e.querySelector('.size-key-sel')?.value || '';
|
|
|
|
|
const url = e.querySelector('.size-url-inp')?.value.trim() || '';
|
|
|
|
|
const type = e.querySelector('.size-mtype-sel')?.value || 'image';
|
|
|
|
|
const cw = e.querySelector('.size-cw-inp')?.value || null;
|
|
|
|
|
const ch = e.querySelector('.size-ch-inp')?.value || null;
|
|
|
|
|
if (key && url) sizes.push({ size_key: key, media_url: url, media_type: type,
|
|
|
|
|
custom_w: cw ? parseInt(cw) : null, custom_h: ch ? parseInt(ch) : null });
|
|
|
|
|
});
|
|
|
|
|
return sizes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Create ─────────────────────────────────────────────────────────────── */
|
|
|
|
|
function clearCreate() {
|
|
|
|
|
document.getElementById('c-title').value = '';
|
|
|
|
|
document.getElementById('c-click-url').value = '';
|
|
|
|
|
document.getElementById('c-note').value = '';
|
|
|
|
|
document.getElementById('c-size-list').innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function submitAd() {
|
|
|
|
|
if (!adminKey) { showAlert('create-alert','error','Enter admin key first.'); return; }
|
|
|
|
|
const body = {
|
|
|
|
|
title: document.getElementById('c-title').value.trim(),
|
|
|
|
|
placement: document.getElementById('c-placement').value,
|
|
|
|
|
click_url: document.getElementById('c-click-url').value.trim(),
|
|
|
|
|
note: document.getElementById('c-note').value.trim(),
|
|
|
|
|
active: true,
|
|
|
|
|
sizes: collectSizes('c-size-list'),
|
|
|
|
|
};
|
|
|
|
|
if (!body.title) { showAlert('create-alert','error','Title is required.'); return; }
|
|
|
|
|
setLoading('create-btn', true);
|
|
|
|
|
apiFetch('', { method:'POST', body: JSON.stringify(body) })
|
|
|
|
|
.then(() => { clearCreate(); setLoading('create-btn',false); showAlert('create-alert','success','Ad saved — reload to see it in the list.'); })
|
|
|
|
|
.catch(e => { setLoading('create-btn',false); showAlert('create-alert','error',e.message); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Edit / save ────────────────────────────────────────────────────────── */
|
|
|
|
|
function toggleEdit(id) {
|
|
|
|
|
const ef = document.getElementById('ef-' + id);
|
|
|
|
|
ef.classList.toggle('open');
|
|
|
|
|
// Wire custom-dims toggles on existing selects when form opens
|
|
|
|
|
ef.querySelectorAll('.size-key-sel').forEach(sel => {
|
|
|
|
|
sel.onchange = () => toggleCustomDims(sel);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveEdit(id) {
|
|
|
|
|
if (!adminKey) { showAlert('ef-alert-'+id,'error','Enter admin key first.'); return; }
|
|
|
|
|
const body = {
|
|
|
|
|
title: document.getElementById('ef-title-'+id).value.trim(),
|
|
|
|
|
placement: document.getElementById('ef-placement-'+id).value,
|
|
|
|
|
click_url: document.getElementById('ef-click-url-'+id).value.trim(),
|
|
|
|
|
note: document.getElementById('ef-note-'+id).value.trim(),
|
|
|
|
|
sizes: collectSizes('ef-size-list-'+id),
|
|
|
|
|
};
|
|
|
|
|
apiFetch('id='+encodeURIComponent(id), { method:'PUT', body: JSON.stringify(body) })
|
|
|
|
|
.then(() => { showAlert('ef-alert-'+id,'success','Saved — reload to refresh the list.'); })
|
|
|
|
|
.catch(e => showAlert('ef-alert-'+id,'error',e.message));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Toggle active ──────────────────────────────────────────────────────── */
|
|
|
|
|
function toggleActive(id, active) {
|
|
|
|
|
if (!adminKey) return;
|
|
|
|
|
const badge = document.getElementById('badge-active-'+id);
|
|
|
|
|
apiFetch('action=toggle', { method:'POST', body: JSON.stringify({id, active}) })
|
|
|
|
|
.then(() => {
|
|
|
|
|
if (badge) { badge.textContent = active ? 'Active' : 'Paused'; badge.className = 'ad-badge ' + (active ? 'ad-badge-active' : 'ad-badge-inactive'); }
|
|
|
|
|
})
|
|
|
|
|
.catch(e => alert('Toggle failed: ' + e.message));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Delete ─────────────────────────────────────────────────────────────── */
|
|
|
|
|
function deleteAd(id) {
|
|
|
|
|
if (!adminKey) { alert('Enter admin key first.'); return; }
|
|
|
|
|
if (!confirm('Delete this ad? This cannot be undone.')) return;
|
|
|
|
|
apiFetch('id='+encodeURIComponent(id), { method:'DELETE' })
|
|
|
|
|
.then(() => {
|
|
|
|
|
const li = document.getElementById('ai-'+id);
|
|
|
|
|
li.style.transition='opacity .2s,transform .2s'; li.style.opacity='0'; li.style.transform='translateX(8px)';
|
|
|
|
|
setTimeout(() => { li.remove(); const cnt=document.getElementById('ad-count'); if(cnt){const n=document.querySelectorAll('.ad-item').length;cnt.textContent=n+' ad'+(n!==1?'s':'')} }, 220);
|
|
|
|
|
})
|
|
|
|
|
.catch(e => alert('Delete failed: ' + e.message));
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
|