Initial dev export (exclude uploads/runtime)
This commit is contained in:
384
modules/newsletter/NewsletterController.php
Normal file
384
modules/newsletter/NewsletterController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
80
modules/newsletter/module.php
Normal file
80
modules/newsletter/module.php
Normal 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']);
|
||||
};
|
||||
96
modules/newsletter/views/admin/edit.php
Normal file
96
modules/newsletter/views/admin/edit.php
Normal 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';
|
||||
259
modules/newsletter/views/admin/index.php
Normal file
259
modules/newsletter/views/admin/index.php
Normal 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';
|
||||
46
modules/newsletter/views/admin/subscribers.php
Normal file
46
modules/newsletter/views/admin/subscribers.php
Normal 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';
|
||||
23
modules/newsletter/views/site/unsubscribe.php
Normal file
23
modules/newsletter/views/site/unsubscribe.php
Normal 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';
|
||||
Reference in New Issue
Block a user