Initial dev export (exclude uploads/runtime)

This commit is contained in:
AudioCore Bot
2026-03-04 20:46:11 +00:00
commit b2afadd539
120 changed files with 20410 additions and 0 deletions

View File

@@ -0,0 +1,384 @@
<?php
declare(strict_types=1);
namespace Modules\Newsletter;
use Core\Http\Response;
use Core\Services\Auth;
use Core\Services\Database;
use Core\Services\Mailer;
use Core\Services\Settings;
use Core\Views\View;
use PDO;
use Throwable;
class NewsletterController
{
private View $view;
public function __construct()
{
$this->view = new View(__DIR__ . '/views');
}
public function subscribe(): Response
{
$email = trim((string)($_POST['email'] ?? ''));
$name = trim((string)($_POST['name'] ?? ''));
if ($email === '') {
return new Response('Missing email', 400);
}
$db = Database::get();
if ($db instanceof PDO) {
$stmt = $db->prepare("
INSERT INTO ac_newsletter_subscribers (email, name, status, source)
VALUES (:email, :name, 'subscribed', 'form')
ON DUPLICATE KEY UPDATE name = VALUES(name), status = 'subscribed', unsubscribed_at = NULL
");
$stmt->execute([
':email' => $email,
':name' => $name !== '' ? $name : null,
]);
}
$this->syncMailchimp($email, $name);
return new Response('Subscribed', 200);
}
public function unsubscribeForm(): Response
{
$email = trim((string)($_GET['email'] ?? ''));
return new Response($this->view->render('site/unsubscribe.php', [
'title' => 'Unsubscribe',
'email' => $email,
'status' => '',
]));
}
public function unsubscribe(): Response
{
$email = trim((string)($_POST['email'] ?? ''));
$status = 'Email is required.';
$db = Database::get();
if ($db instanceof PDO && $email !== '') {
$stmt = $db->prepare("UPDATE ac_newsletter_subscribers SET status = 'unsubscribed', unsubscribed_at = NOW() WHERE email = :email");
$stmt->execute([':email' => $email]);
$status = 'You have been unsubscribed.';
}
return new Response($this->view->render('site/unsubscribe.php', [
'title' => 'Unsubscribe',
'email' => $email,
'status' => $status,
]));
}
public function adminIndex(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
$campaigns = [];
if ($db instanceof PDO) {
$stmt = $db->query("SELECT id, title, subject, status, sent_at, scheduled_at FROM ac_newsletter_campaigns ORDER BY created_at DESC");
$campaigns = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
}
return new Response($this->view->render('admin/index.php', [
'title' => 'Newsletter',
'campaigns' => $campaigns,
]));
}
public function adminSubscribers(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
$subscribers = [];
if ($db instanceof PDO) {
$stmt = $db->query("SELECT id, email, name, status, created_at FROM ac_newsletter_subscribers ORDER BY created_at DESC");
$subscribers = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
}
return new Response($this->view->render('admin/subscribers.php', [
'title' => 'Newsletter',
'subscribers' => $subscribers,
]));
}
public function adminEdit(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$campaign = [
'id' => 0,
'title' => '',
'subject' => '',
'content_html' => '',
'status' => 'draft',
'scheduled_at' => '',
];
$db = Database::get();
if ($id > 0 && $db instanceof PDO) {
$stmt = $db->prepare("SELECT * FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$campaign = $row;
}
}
return new Response($this->view->render('admin/edit.php', [
'title' => $id > 0 ? 'Edit Campaign' : 'New Campaign',
'campaign' => $campaign,
'error' => '',
]));
}
public function adminSave(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
if (!$db instanceof PDO) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$id = (int)($_POST['id'] ?? 0);
$title = trim((string)($_POST['title'] ?? ''));
$subject = trim((string)($_POST['subject'] ?? ''));
$content = (string)($_POST['content_html'] ?? '');
$scheduledAt = trim((string)($_POST['scheduled_at'] ?? ''));
if ($title === '' || $subject === '') {
return $this->renderEditError($id, $title, $subject, $content, 'Title and subject are required.');
}
try {
$status = $scheduledAt !== '' ? 'scheduled' : 'draft';
if ($id > 0) {
$stmt = $db->prepare("
UPDATE ac_newsletter_campaigns
SET title = :title, subject = :subject, content_html = :content,
status = :status, scheduled_at = :scheduled_at
WHERE id = :id
");
$stmt->execute([
':title' => $title,
':subject' => $subject,
':content' => $content,
':status' => $status,
':scheduled_at' => $scheduledAt !== '' ? $scheduledAt : null,
':id' => $id,
]);
} else {
$stmt = $db->prepare("
INSERT INTO ac_newsletter_campaigns (title, subject, content_html, status, scheduled_at)
VALUES (:title, :subject, :content, :status, :scheduled_at)
");
$stmt->execute([
':title' => $title,
':subject' => $subject,
':content' => $content,
':status' => $status,
':scheduled_at' => $scheduledAt !== '' ? $scheduledAt : null,
]);
}
} catch (Throwable $e) {
return $this->renderEditError($id, $title, $subject, $content, 'Unable to save campaign.');
}
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
public function adminSend(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
if (!$db instanceof PDO) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$id = (int)($_POST['id'] ?? 0);
$stmt = $db->prepare("SELECT id, subject, content_html FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$campaign = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$campaign) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$subStmt = $db->query("SELECT id, email FROM ac_newsletter_subscribers WHERE status = 'subscribed'");
$subs = $subStmt ? $subStmt->fetchAll(PDO::FETCH_ASSOC) : [];
$settings = [
'smtp_host' => Settings::get('smtp_host'),
'smtp_port' => Settings::get('smtp_port'),
'smtp_user' => Settings::get('smtp_user'),
'smtp_pass' => Settings::get('smtp_pass'),
'smtp_encryption' => Settings::get('smtp_encryption'),
'smtp_from_email' => Settings::get('smtp_from_email'),
'smtp_from_name' => Settings::get('smtp_from_name'),
];
foreach ($subs as $sub) {
$result = Mailer::send((string)$sub['email'], (string)$campaign['subject'], (string)$campaign['content_html'], $settings);
$sendStmt = $db->prepare("
INSERT INTO ac_newsletter_sends (campaign_id, subscriber_id, status, sent_at, error_text)
VALUES (:campaign_id, :subscriber_id, :status, NOW(), :error_text)
");
$sendStmt->execute([
':campaign_id' => (int)$campaign['id'],
':subscriber_id' => (int)$sub['id'],
':status' => $result['ok'] ? 'sent' : 'failed',
':error_text' => $result['ok'] ? null : (string)$result['error'],
]);
}
$db->prepare("UPDATE ac_newsletter_campaigns SET status = 'sent', sent_at = NOW() WHERE id = :id")
->execute([':id' => $id]);
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
public function adminTestSend(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
if (!$db instanceof PDO) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$id = (int)($_POST['id'] ?? 0);
$email = trim((string)($_POST['test_email'] ?? ''));
if ($id <= 0 || $email === '') {
return new Response('', 302, ['Location' => '/admin/newsletter/campaigns/edit?id=' . $id]);
}
$stmt = $db->prepare("SELECT subject, content_html FROM ac_newsletter_campaigns WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$campaign = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$campaign) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$settings = [
'smtp_host' => Settings::get('smtp_host'),
'smtp_port' => Settings::get('smtp_port'),
'smtp_user' => Settings::get('smtp_user'),
'smtp_pass' => Settings::get('smtp_pass'),
'smtp_encryption' => Settings::get('smtp_encryption'),
'smtp_from_email' => Settings::get('smtp_from_email'),
'smtp_from_name' => Settings::get('smtp_from_name'),
];
Mailer::send($email, (string)$campaign['subject'], (string)$campaign['content_html'], $settings);
return new Response('', 302, ['Location' => '/admin/newsletter/campaigns/edit?id=' . $id]);
}
public function adminProcessQueue(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$db = Database::get();
if (!$db instanceof PDO) {
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
$stmt = $db->prepare("
SELECT id FROM ac_newsletter_campaigns
WHERE status = 'scheduled' AND scheduled_at IS NOT NULL AND scheduled_at <= NOW()
");
$stmt->execute();
$campaigns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($campaigns as $campaign) {
$_POST['id'] = (int)$campaign['id'];
$this->adminSend();
}
return new Response('', 302, ['Location' => '/admin/newsletter']);
}
public function adminDeleteSubscriber(): Response
{
if ($guard = $this->guard(['admin', 'manager'])) {
return $guard;
}
$id = (int)($_POST['id'] ?? 0);
$db = Database::get();
if ($db instanceof PDO && $id > 0) {
$db->prepare("DELETE FROM ac_newsletter_subscribers WHERE id = :id")->execute([':id' => $id]);
}
return new Response('', 302, ['Location' => '/admin/newsletter/subscribers']);
}
private function renderEditError(int $id, string $title, string $subject, string $content, string $error): Response
{
$campaign = [
'id' => $id,
'title' => $title,
'subject' => $subject,
'content_html' => $content,
];
return new Response($this->view->render('admin/edit.php', [
'title' => $id > 0 ? 'Edit Campaign' : 'New Campaign',
'campaign' => $campaign,
'error' => $error,
]));
}
private function guard(array $roles): ?Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole($roles)) {
return new Response('', 302, ['Location' => '/admin']);
}
return null;
}
private function syncMailchimp(string $email, string $name): void
{
$apiKey = Settings::get('mailchimp_api_key');
$listId = Settings::get('mailchimp_list_id');
if ($apiKey === '' || $listId === '') {
return;
}
$parts = explode('-', $apiKey);
$dc = $parts[1] ?? '';
if ($dc === '') {
return;
}
$subscriberHash = md5(strtolower($email));
$url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$listId}/members/{$subscriberHash}";
$payload = json_encode([
'email_address' => $email,
'status' => 'subscribed',
'merge_fields' => [
'FNAME' => $name,
],
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, 'user:' . $apiKey);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_exec($ch);
curl_close($ch);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
use Core\Http\Router;
use Core\Services\Shortcodes;
use Modules\Newsletter\NewsletterController;
require_once __DIR__ . '/NewsletterController.php';
Shortcodes::register('newsletter-signup', static function (array $attrs = []): string {
$title = trim((string)($attrs['title'] ?? 'Join the newsletter'));
$button = trim((string)($attrs['button'] ?? 'Subscribe'));
$placeholder = trim((string)($attrs['placeholder'] ?? 'you@example.com'));
if ($title === '') {
$title = 'Join the newsletter';
}
if ($button === '') {
$button = 'Subscribe';
}
if ($placeholder === '') {
$placeholder = 'you@example.com';
}
return '<form method="post" action="/newsletter/subscribe" class="ac-shortcode-newsletter-form">'
. '<div class="ac-shortcode-newsletter-title">' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</div>'
. '<div class="ac-shortcode-newsletter-row">'
. '<input type="email" name="email" required class="ac-shortcode-newsletter-input" placeholder="' . htmlspecialchars($placeholder, ENT_QUOTES, 'UTF-8') . '">'
. '<button type="submit" class="ac-shortcode-newsletter-btn">' . htmlspecialchars($button, ENT_QUOTES, 'UTF-8') . '</button>'
. '</div>'
. '</form>';
});
Shortcodes::register('newsletter-unsubscribe', static function (array $attrs = []): string {
$label = trim((string)($attrs['label'] ?? 'Unsubscribe'));
if ($label === '') {
$label = 'Unsubscribe';
}
$token = trim((string)($attrs['token'] ?? ''));
$href = '/newsletter/unsubscribe';
if ($token !== '') {
$href .= '?token=' . rawurlencode($token);
}
return '<a class="ac-shortcode-link ac-shortcode-newsletter-unsubscribe" href="' . htmlspecialchars($href, ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</a>';
});
Shortcodes::register('newsletter-unsubscribe-form', static function (array $attrs = []): string {
$title = trim((string)($attrs['title'] ?? 'Unsubscribe from newsletter'));
$button = trim((string)($attrs['button'] ?? 'Unsubscribe'));
if ($title === '') {
$title = 'Unsubscribe from newsletter';
}
if ($button === '') {
$button = 'Unsubscribe';
}
return '<form method="post" action="/newsletter/unsubscribe" class="ac-shortcode-newsletter-form">'
. '<div class="ac-shortcode-newsletter-title">' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</div>'
. '<div class="ac-shortcode-newsletter-row">'
. '<input type="email" name="email" required class="ac-shortcode-newsletter-input" placeholder="you@example.com">'
. '<button type="submit" class="ac-shortcode-newsletter-btn">' . htmlspecialchars($button, ENT_QUOTES, 'UTF-8') . '</button>'
. '</div>'
. '</form>';
});
return function (Router $router): void {
$controller = new NewsletterController();
$router->post('/newsletter/subscribe', [$controller, 'subscribe']);
$router->get('/newsletter/unsubscribe', [$controller, 'unsubscribeForm']);
$router->post('/newsletter/unsubscribe', [$controller, 'unsubscribe']);
$router->get('/admin/newsletter', [$controller, 'adminIndex']);
$router->get('/admin/newsletter/campaigns/new', [$controller, 'adminEdit']);
$router->get('/admin/newsletter/campaigns/edit', [$controller, 'adminEdit']);
$router->post('/admin/newsletter/campaigns/save', [$controller, 'adminSave']);
$router->post('/admin/newsletter/campaigns/send', [$controller, 'adminSend']);
$router->post('/admin/newsletter/campaigns/test', [$controller, 'adminTestSend']);
$router->post('/admin/newsletter/campaigns/process', [$controller, 'adminProcessQueue']);
$router->get('/admin/newsletter/subscribers', [$controller, 'adminSubscribers']);
$router->post('/admin/newsletter/subscribers/delete', [$controller, 'adminDeleteSubscriber']);
};

View File

@@ -0,0 +1,96 @@
<?php
$pageTitle = $title ?? 'Edit Campaign';
$campaign = $campaign ?? [];
$error = $error ?? '';
ob_start();
?>
<section class="admin-card">
<div class="badge">Newsletter</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
<p style="color: var(--muted); margin-top:6px;">Write a full HTML campaign.</p>
</div>
<a href="/admin/newsletter" class="btn outline">Back</a>
</div>
<?php if ($error): ?>
<div style="margin-top:16px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<form method="post" action="/admin/newsletter/campaigns/save" style="margin-top:18px; display:grid; gap:16px;">
<input type="hidden" name="id" value="<?= (int)($campaign['id'] ?? 0) ?>">
<div class="admin-card" style="padding:16px;">
<div style="display:grid; gap:12px;">
<label class="label">Title</label>
<input class="input" name="title" value="<?= htmlspecialchars((string)($campaign['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Monthly Update">
<label class="label">Subject</label>
<input class="input" name="subject" value="<?= htmlspecialchars((string)($campaign['subject'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="AudioCore Newsletter">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<label class="label" style="margin:0;">Content (HTML)</label>
<button type="button" class="btn outline small" data-media-picker="newsletter_content_html">Insert Media</button>
</div>
<textarea class="input" id="newsletter_content_html" name="content_html" rows="18" style="resize:vertical; font-family:'IBM Plex Mono', monospace; font-size:13px; line-height:1.6;"><?= htmlspecialchars((string)($campaign['content_html'] ?? ''), ENT_QUOTES, 'UTF-8') ?></textarea>
<label class="label">Schedule Send (optional)</label>
<input class="input" name="scheduled_at" value="<?= htmlspecialchars((string)($campaign['scheduled_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="2026-01-25 18:30:00">
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:12px; align-items:center;">
<button type="button" id="previewNewsletter" class="btn outline">Preview</button>
<button type="submit" class="btn">Save campaign</button>
</div>
</form>
<?php if (!empty($campaign['id'])): ?>
<form method="post" action="/admin/newsletter/campaigns/test" style="margin-top:12px; display:flex; gap:10px; align-items:center;">
<input type="hidden" name="id" value="<?= (int)($campaign['id'] ?? 0) ?>">
<input class="input" name="test_email" placeholder="test@example.com" style="max-width:280px;">
<button type="submit" class="btn outline small">Send Test</button>
</form>
<?php endif; ?>
</section>
<script>
(function () {
const previewBtn = document.getElementById('previewNewsletter');
const contentEl = document.querySelector('textarea[name="content_html"]');
if (!previewBtn || !contentEl) {
return;
}
previewBtn.addEventListener('click', function () {
const previewWindow = window.open('', 'newsletterPreview', 'width=1000,height=800');
if (!previewWindow) {
return;
}
const html = contentEl.value || '';
const doc = previewWindow.document;
doc.open();
doc.write(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Newsletter Preview</title>
<style>
body { margin: 0; background: #f0f2f5; font-family: Arial, sans-serif; }
.wrap { padding: 24px; display: flex; justify-content: center; }
.frame { max-width: 680px; width: 100%; background: #ffffff; border-radius: 12px; padding: 24px; box-shadow: 0 12px 30px rgba(0,0,0,0.15); }
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
<div class="wrap">
<div class="frame">
${html}
</div>
</div>
</body>
</html>`);
doc.close();
});
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../admin/views/layout.php';

View File

@@ -0,0 +1,259 @@
<?php
$pageTitle = 'Newsletter';
$campaigns = $campaigns ?? [];
ob_start();
?>
<section class="admin-card">
<div class="badge">Newsletter</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;">Campaigns</h1>
<p style="color: var(--muted); margin-top:6px;">Build and send HTML newsletters.</p>
</div>
<div style="display:flex; gap:10px;">
<a href="/admin/newsletter/subscribers" class="btn outline small">Subscribers</a>
<form method="post" action="/admin/newsletter/campaigns/process" style="display:inline;">
<button type="submit" class="btn outline small">Process Queue</button>
</form>
<a href="/admin/newsletter/campaigns/new" class="btn small">New Campaign</a>
</div>
</div>
<div style="margin-top:18px; display:grid; gap:10px;">
<div style="display:grid; grid-template-columns: 2fr 1.4fr 120px 140px 140px 140px; gap:12px; font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<div>Title</div>
<div>Subject</div>
<div>Status</div>
<div>Scheduled</div>
<div>Sent At</div>
<div>Actions</div>
</div>
<?php if (!$campaigns): ?>
<div style="color: var(--muted); font-size:13px;">No campaigns yet.</div>
<?php else: ?>
<?php foreach ($campaigns as $campaign): ?>
<div style="display:grid; grid-template-columns: 2fr 1.4fr 120px 140px 140px 140px; gap:12px; align-items:center; padding:10px 12px; border-radius:16px; border:1px solid var(--stroke); background: rgba(14,14,16,0.9);">
<div style="font-weight:600;"><?= htmlspecialchars((string)($campaign['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($campaign['subject'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:<?= ((string)($campaign['status'] ?? '') === 'sent') ? 'var(--accent-2)' : 'var(--muted)' ?>;">
<?= htmlspecialchars((string)($campaign['status'] ?? 'draft'), ENT_QUOTES, 'UTF-8') ?>
</div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($campaign['scheduled_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($campaign['sent_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="display:flex; gap:8px;">
<a href="/admin/newsletter/campaigns/edit?id=<?= (int)$campaign['id'] ?>" class="btn outline small">Edit</a>
<form method="post" action="/admin/newsletter/campaigns/send" onsubmit="return confirm('Send this campaign now?');">
<input type="hidden" name="id" value="<?= (int)$campaign['id'] ?>">
<button type="submit" class="btn small">Send</button>
</form>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
<section class="admin-card" style="margin-top:16px;">
<div class="badge">Signup Form</div>
<p style="color: var(--muted); margin-top:10px;">Choose a signup form template to paste into any custom page.</p>
<div style="margin-top:12px; display:grid; gap:12px;">
<select id="signupTemplateSelect" class="input" style="text-transform:none;">
<option value="">Select template</option>
<option value="signup-compact">Compact Inline</option>
<option value="signup-card">Card Form</option>
<option value="signup-minimal">Minimal</option>
<option value="signup-banner">Banner CTA</option>
</select>
<div id="signupTemplatePreview" style="border:1px solid rgba(255,255,255,0.12); border-radius:14px; padding:12px; background: rgba(0,0,0,0.2); min-height:120px;"></div>
<div style="display:flex; gap:10px; justify-content:flex-end;">
<button type="button" id="copySignupTemplate" class="btn outline small">Copy HTML</button>
</div>
</div>
</section>
<section class="admin-card" style="margin-top:16px;">
<div class="badge">Template Starter</div>
<p style="color: var(--muted); margin-top:10px;">Pick a campaign template, preview it, then copy HTML.</p>
<div style="margin-top:12px; display:grid; gap:12px;">
<select id="newsletterTemplateSelect" class="input" style="text-transform:none;">
<option value="">Select template</option>
<option value="email-minimal">Minimal Update</option>
<option value="email-feature">Feature Promo</option>
<option value="email-digest">Weekly Digest</option>
<option value="email-event">Event Invite</option>
</select>
<div id="newsletterTemplatePreview" style="border:1px solid rgba(255,255,255,0.12); border-radius:14px; padding:12px; background: rgba(0,0,0,0.2); min-height:140px;"></div>
<div style="display:flex; gap:10px; justify-content:flex-end;">
<button type="button" id="copyNewsletterTemplate" class="btn outline small">Copy HTML</button>
</div>
</div>
</section>
<script>
(function () {
const signupTemplates = {
'signup-compact': {
html: `<form method="post" action="/newsletter/subscribe" style="display:flex; gap:8px; flex-wrap:wrap;">
<input type="text" name="name" placeholder="Name" style="padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb;">
<input type="email" name="email" placeholder="Email" required style="padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb;">
<button type="submit" style="padding:10px 16px; border-radius:999px; border:none; background:#22f2a5; color:#071016;">Subscribe</button>
</form>`,
preview: `<div style="background:#0f1117; padding:12px; border-radius:12px;">
<div style="color:#c9cbd4; font-size:12px; margin-bottom:8px;">Compact Inline</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<div style="flex:1; min-width:120px; height:34px; border-radius:8px; background:#1b1e26;"></div>
<div style="flex:1; min-width:120px; height:34px; border-radius:8px; background:#1b1e26;"></div>
<div style="width:110px; height:34px; border-radius:999px; background:#22f2a5;"></div>
</div>
</div>`
},
'signup-card': {
html: `<div style="padding:18px; border:1px solid #e5e7eb; border-radius:12px;">
<h3 style="margin:0 0 8px;">Join the newsletter</h3>
<p style="margin:0 0 12px;">Monthly updates and releases.</p>
<form method="post" action="/newsletter/subscribe" style="display:grid; gap:8px;">
<input type="text" name="name" placeholder="Name" style="padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb;">
<input type="email" name="email" placeholder="Email" required style="padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb;">
<button type="submit" style="padding:10px 16px; border-radius:999px; border:none; background:#22f2a5; color:#071016;">Subscribe</button>
</form>
</div>`,
preview: `<div style="background:#0f1117; padding:12px; border-radius:12px;">
<div style="height:10px; width:120px; background:#2a2e3a; border-radius:6px; margin-bottom:10px;"></div>
<div style="height:8px; width:180px; background:#1b1e26; border-radius:6px; margin-bottom:12px;"></div>
<div style="display:grid; gap:8px;">
<div style="height:34px; border-radius:8px; background:#1b1e26;"></div>
<div style="height:34px; border-radius:8px; background:#1b1e26;"></div>
<div style="height:34px; width:120px; border-radius:999px; background:#22f2a5;"></div>
</div>
</div>`
},
'signup-minimal': {
html: `<form method="post" action="/newsletter/subscribe">
<input type="email" name="email" placeholder="Email" required style="padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb; width:100%; max-width:320px;">
<button type="submit" style="margin-top:8px; padding:8px 14px; border-radius:999px; border:1px solid #111; background:#111; color:#fff;">Subscribe</button>
</form>`,
preview: `<div style="background:#0f1117; padding:12px; border-radius:12px;">
<div style="height:34px; width:240px; border-radius:8px; background:#1b1e26;"></div>
<div style="height:30px; width:110px; border-radius:999px; background:#2a2e3a; margin-top:8px;"></div>
</div>`
},
'signup-banner': {
html: `<div style="padding:16px; border-radius:12px; background:#0f172a; color:#fff; display:flex; flex-wrap:wrap; gap:12px; align-items:center;">
<div style="flex:1; min-width:180px;">
<strong>Get updates</strong><br>
New releases, events, and drops.
</div>
<form method="post" action="/newsletter/subscribe" style="display:flex; gap:8px; flex-wrap:wrap;">
<input type="email" name="email" placeholder="Email" required style="padding:10px 12px; border-radius:10px; border:1px solid #1f2937;">
<button type="submit" style="padding:10px 16px; border-radius:999px; border:none; background:#22f2a5; color:#071016;">Subscribe</button>
</form>
</div>`,
preview: `<div style="background:#0f1117; padding:12px; border-radius:12px;">
<div style="height:16px; width:160px; background:#1b1e26; border-radius:6px; margin-bottom:8px;"></div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<div style="height:34px; width:160px; border-radius:8px; background:#1b1e26;"></div>
<div style="height:34px; width:110px; border-radius:999px; background:#22f2a5;"></div>
</div>
</div>`
}
};
const emailTemplates = {
'email-minimal': {
html: `<div style="font-family:Arial, sans-serif; color:#111; line-height:1.6;">
<h1 style="margin:0 0 8px;">AudioCore Update</h1>
<p style="margin:0 0 16px;">Latest releases, news, and announcements.</p>
<p style="margin:0;">Thanks for listening.</p>
</div>`,
preview: `<div style="background:#fff; color:#111; padding:12px; border-radius:8px;">
<div style="height:16px; width:180px; background:#e5e7eb; border-radius:6px; margin-bottom:8px;"></div>
<div style="height:10px; width:240px; background:#f3f4f6; border-radius:6px; margin-bottom:10px;"></div>
<div style="height:10px; width:120px; background:#f3f4f6; border-radius:6px;"></div>
</div>`
},
'email-feature': {
html: `<div style="font-family:Arial, sans-serif; color:#111; line-height:1.6;">
<h1 style="margin:0 0 8px;">Featured Release</h1>
<img src="https://placehold.co/600x360/111827/ffffff?text=Cover" alt="" style="width:100%; border-radius:10px; margin:8px 0;">
<p style="margin:0 0 12px;">REC008 Night Drive EP now available.</p>
<a href="#" style="display:inline-block; padding:10px 16px; border-radius:999px; background:#22f2a5; color:#071016; text-decoration:none;">Listen now</a>
</div>`,
preview: `<div style="background:#fff; color:#111; padding:12px; border-radius:8px;">
<div style="height:16px; width:160px; background:#e5e7eb; border-radius:6px; margin-bottom:8px;"></div>
<div style="height:120px; background:#e5e7eb; border-radius:8px; margin-bottom:10px;"></div>
<div style="height:10px; width:200px; background:#f3f4f6; border-radius:6px; margin-bottom:12px;"></div>
<div style="height:30px; width:120px; background:#22f2a5; border-radius:999px;"></div>
</div>`
},
'email-digest': {
html: `<div style="font-family:Arial, sans-serif; color:#111; line-height:1.6;">
<h1 style="margin:0 0 12px;">Weekly Digest</h1>
<ul style="padding-left:18px; margin:0 0 12px;">
<li>New release: REC009 Twilight Runner</li>
<li>Label spotlight: Neon District</li>
<li>Playlist update: Midnight Circuit</li>
</ul>
<p style="margin:0;">See the full catalog for more.</p>
</div>`,
preview: `<div style="background:#fff; color:#111; padding:12px; border-radius:8px;">
<div style="height:16px; width:160px; background:#e5e7eb; border-radius:6px; margin-bottom:10px;"></div>
<div style="height:10px; width:240px; background:#f3f4f6; border-radius:6px; margin-bottom:6px;"></div>
<div style="height:10px; width:200px; background:#f3f4f6; border-radius:6px; margin-bottom:6px;"></div>
<div style="height:10px; width:180px; background:#f3f4f6; border-radius:6px; margin-bottom:12px;"></div>
<div style="height:10px; width:200px; background:#f3f4f6; border-radius:6px;"></div>
</div>`
},
'email-event': {
html: `<div style="font-family:Arial, sans-serif; color:#111; line-height:1.6;">
<h1 style="margin:0 0 8px;">Live Showcase</h1>
<p style="margin:0 0 12px;">Friday, 8PM · Warehouse 12</p>
<a href="#" style="display:inline-block; padding:10px 16px; border-radius:999px; background:#111; color:#fff; text-decoration:none;">RSVP</a>
</div>`,
preview: `<div style="background:#fff; color:#111; padding:12px; border-radius:8px;">
<div style="height:16px; width:140px; background:#e5e7eb; border-radius:6px; margin-bottom:8px;"></div>
<div style="height:10px; width:160px; background:#f3f4f6; border-radius:6px; margin-bottom:12px;"></div>
<div style="height:30px; width:80px; background:#111; border-radius:999px;"></div>
</div>`
}
};
function wirePicker(selectId, previewId, copyId, templates) {
const select = document.getElementById(selectId);
const preview = document.getElementById(previewId);
const copyBtn = document.getElementById(copyId);
if (!select || !preview || !copyBtn) {
return;
}
function renderPreview() {
const key = select.value;
preview.innerHTML = key && templates[key] ? templates[key].preview : '';
}
select.addEventListener('change', renderPreview);
copyBtn.addEventListener('click', async function () {
const key = select.value;
if (!key || !templates[key]) {
return;
}
const html = templates[key].html;
try {
await navigator.clipboard.writeText(html);
copyBtn.textContent = 'Copied';
setTimeout(() => (copyBtn.textContent = 'Copy HTML'), 1200);
} catch (err) {
copyBtn.textContent = 'Copy failed';
setTimeout(() => (copyBtn.textContent = 'Copy HTML'), 1200);
}
});
renderPreview();
}
wirePicker('signupTemplateSelect', 'signupTemplatePreview', 'copySignupTemplate', signupTemplates);
wirePicker('newsletterTemplateSelect', 'newsletterTemplatePreview', 'copyNewsletterTemplate', emailTemplates);
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../admin/views/layout.php';

View File

@@ -0,0 +1,46 @@
<?php
$pageTitle = 'Newsletter';
$subscribers = $subscribers ?? [];
ob_start();
?>
<section class="admin-card">
<div class="badge">Newsletter</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;">Subscribers</h1>
<p style="color: var(--muted); margin-top:6px;">People on your newsletter list.</p>
</div>
<a href="/admin/newsletter" class="btn outline small">Back to Campaigns</a>
</div>
<div style="margin-top:18px; display:grid; gap:10px;">
<div style="display:grid; grid-template-columns: 1.5fr 1.5fr 120px 160px 120px; gap:12px; font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<div>Email</div>
<div>Name</div>
<div>Status</div>
<div>Joined</div>
<div>Actions</div>
</div>
<?php if (!$subscribers): ?>
<div style="color: var(--muted); font-size:13px;">No subscribers yet.</div>
<?php else: ?>
<?php foreach ($subscribers as $sub): ?>
<div style="display:grid; grid-template-columns: 1.5fr 1.5fr 120px 160px 120px; gap:12px; align-items:center; padding:10px 12px; border-radius:16px; border:1px solid var(--stroke); background: rgba(14,14,16,0.9);">
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($sub['email'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($sub['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:<?= ((string)($sub['status'] ?? '') === 'subscribed') ? 'var(--accent-2)' : 'var(--muted)' ?>;">
<?= htmlspecialchars((string)($sub['status'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
</div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($sub['created_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
<form method="post" action="/admin/newsletter/subscribers/delete" onsubmit="return confirm('Delete this subscriber?');">
<input type="hidden" name="id" value="<?= (int)($sub['id'] ?? 0) ?>">
<button type="submit" class="btn outline small">Delete</button>
</form>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../admin/views/layout.php';

View File

@@ -0,0 +1,23 @@
<?php
$pageTitle = $title ?? 'Unsubscribe';
$email = $email ?? '';
$status = $status ?? '';
ob_start();
?>
<section class="card">
<div class="badge">Newsletter</div>
<h1 style="margin-top:12px; font-size:30px;">Unsubscribe</h1>
<p style="color:var(--muted); margin-top:8px;">Remove your email from the newsletter list.</p>
<?php if ($status !== ''): ?>
<div style="margin-top:12px; color:var(--muted);"><?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<form method="post" action="/newsletter/unsubscribe" style="margin-top:14px; display:grid; gap:10px;">
<input class="input" name="email" value="<?= htmlspecialchars((string)$email, ENT_QUOTES, 'UTF-8') ?>" placeholder="you@example.com">
<button type="submit" class="btn" style="width:max-content;">Unsubscribe</button>
</form>
</section>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../views/site/layout.php';