v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/language.php';
- require_once __DIR__ . '/includes/font-library.php';
- require_once __DIR__ . '/includes/footer-links.php';
- require_once __DIR__ . '/includes/version.php';
- require_once __DIR__ . '/includes/ai.php';
- require_once __DIR__ . '/includes/currency.php';
- require_once __DIR__ . '/includes/mailer.php';
-
- Auth::boot();
- Auth::requireRoleAtLeast(Auth::ROLE_STORE_OWNER);
-
- $currentUser = Auth::user();
- $currentRole = Auth::role();
- $db = Database::site();
- $db->exec(
- 'CREATE TABLE IF NOT EXISTS site_settings (
- setting_key VARCHAR(120) NOT NULL PRIMARY KEY,
- setting_value LONGTEXT NULL,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
- );
-
- $flashMessage = null;
- $flashType = 'success';
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
- $flashMessage = 'Your session expired. Please try again.';
- $flashType = 'error';
- } else {
- $action = $_POST['action'] ?? '';
-
- if ($action === 'save_settings') {
- $aiSaveError = null;
- require __DIR__ . '/dashboard-actions/save-settings.php';
- if ($aiSaveError !== null) {
- $flashMessage = $aiSaveError;
- $flashType = 'error';
- } else {
- $flashMessage = 'Settings saved.';
- }
- }
-
- if ($action === 'save_language') {
- require __DIR__ . '/dashboard-actions/save-language.php';
- $flashMessage = 'Texts saved.';
- }
-
- if ($action === 'save_footer') {
- require __DIR__ . '/dashboard-actions/save-footer.php';
- $flashMessage = 'Footer saved.';
- }
-
- if ($action === 'add_nav_item') {
- require __DIR__ . '/dashboard-actions/save-nav.php';
- $flashMessage = 'Menu item added.';
- }
-
- if ($action === 'move_nav_item') {
- require __DIR__ . '/dashboard-actions/save-nav.php';
- }
-
- if ($action === 'delete_nav_item') {
- require __DIR__ . '/dashboard-actions/save-nav.php';
- $flashMessage = 'Menu item removed.';
- }
-
- if ($action === 'update_nav_item_icon') {
- require __DIR__ . '/dashboard-actions/save-nav.php';
- $flashMessage = 'Icon updated.';
- }
-
- if ($action === 'send_test_email') {
- require __DIR__ . '/dashboard-actions/send-test-email.php';
- }
- }
- }
-
- $settingsRows = $db->query('SELECT setting_key, setting_value FROM site_settings')->fetchAll();
- $settings = [];
- foreach ($settingsRows as $row) {
- $settings[$row['setting_key']] = $row['setting_value'];
- }
-
- $defaults = [
- 'site_name' => Config::get('APP_NAME', 'StocketBase'),
- 'site_description' => '',
- 'seo_title' => '',
- 'seo_description' => '',
- 'seo_keywords' => '',
- 'seo_author' => '',
- 'seo_reply_to' => '',
- 'seo_application_name' => '',
- 'seo_robots_index' => '1',
- 'seo_robots_follow' => '1',
- 'seo_og_product_image_enabled' => '0',
- 'clean_product_urls' => '0',
- 'llms_txt_enabled' => '0',
- 'site_logo_url' => '',
- 'site_show_name_with_logo' => '0',
- 'site_short_name' => '',
- 'site_short_name_enabled' => '0',
- 'registration_enabled' => '1',
- 'login_enabled' => '1',
- 'require_account_to_purchase' => '0',
- 'download_link_expiry_days' => '30',
- 'download_link_max_downloads' => '10',
- 'contact_enabled' => '0',
- 'contact_recipient_email' => '',
- 'contact_provider' => 'php_mail',
- 'contact_subject_mode' => 'free',
- 'contact_subjects' => '',
- 'contact_from_email' => '',
- 'contact_resend_api_key' => '',
- 'contact_postmark_token' => '',
- 'custom_css' => '',
- 'custom_js' => '',
- 'custom_head_html' => '',
- 'custom_head_html_index' => '0',
- 'custom_head_html_product' => '0',
- 'custom_head_html_category' => '0',
- 'custom_head_html_pages' => '0',
- 'custom_body_html' => '',
- 'custom_body_html_index' => '0',
- 'custom_body_html_product' => '0',
- 'custom_body_html_category' => '0',
- 'custom_body_html_pages' => '0',
- 'menu_position' => 'top',
- 'featured_banner_enabled' => '0',
- 'featured_banner_mode' => 'static',
- 'featured_banner_hide_duplicates' => '0',
- 'featured_banner_product_1' => '0',
- 'featured_banner_product_2' => '0',
- 'featured_banner_product_3' => '0',
- 'featured_banner_product_4' => '0',
- 'featured_banner_product_5' => '0',
- 'featured_banner_auto_window_value' => '24',
- 'featured_banner_auto_window_unit' => 'hours',
- 'featured_banner_auto_category_mode' => 'mixed',
- 'related_products_enabled' => '1',
- 'product_show_buyers' => '0',
- 'product_show_downloads' => '0',
- 'back_to_top_enabled' => '0',
- 'back_to_top_progress_enabled' => '0',
- 'breadcrumbs_enabled' => '0',
- 'tags_enabled' => '0',
- 'activity_log_enabled' => '0',
- 'seo_preview_enabled' => '0',
- 'indexnow_enabled' => '0',
- 'indexnow_key' => '',
- 'sitemap_rss_enabled' => '1',
- 'reviews_enabled' => '1',
- 'store_category' => 'general',
- 'pricing_model' => 'per_product',
- 'store_currency' => 'USD',
- 'payment_provider' => 'stripe',
- 'payment_webhook_secret' => '',
- 'payment_polar_webhook_secret' => '',
- 'payment_stripe_secret_key' => '',
- 'payment_stripe_publishable_key' => '',
- 'payment_polar_access_token' => '',
- 'payment_polar_organization_id' => '',
- 'subscription_interval' => 'month',
- 'mail_transport' => 'smtp',
- 'mail_smtp_host' => '',
- 'mail_smtp_port' => '587',
- 'mail_smtp_username' => '',
- 'mail_smtp_password' => '',
- 'mail_smtp_encryption' => 'tls',
- 'mail_resend_api_key' => '',
- 'mail_postmark_server_token' => '',
- 'mail_from_email' => '',
- 'mail_from_name' => '',
- 'mail_reply_to' => '',
- 'heading_font' => 'Plus Jakarta Sans',
- 'body_font' => 'PT Serif',
- 'interface_font' => 'Plus Jakarta Sans',
- 'footer_columns' => '[]',
- 'footer_social_links' => '[]',
- 'product_loading_mode' => 'scroll',
- 'pagination_style' => 'numbered',
- 'products_per_page_home' => '12',
- 'products_per_page_category' => '12',
- 'search_enabled' => '0',
- 'search_match_title' => '1',
- 'search_match_excerpt' => '1',
- 'search_match_content' => '0',
- 'search_style' => 'icon',
- 'search_show_in_nav' => '0',
- 'search_show_in_footer' => '0',
- 'share_enabled' => '0',
- 'share_facebook' => '1',
- 'share_twitter' => '1',
- 'share_linkedin' => '1',
- 'share_whatsapp' => '1',
- 'share_telegram' => '0',
- 'share_email' => '1',
- 'share_copy_link' => '1',
- 'pwa_enabled' => '0',
- 'pwa_name' => '',
- 'pwa_short_name' => '',
- 'pwa_description' => '',
- 'pwa_icon_url' => '',
- 'pwa_theme_color' => '#ffffff',
- 'pwa_background_color' => '#ffffff',
- 'app_banner_enabled' => '0',
- 'app_android_url' => '',
- 'app_ios_url' => '',
- ];
- $missingSettings = array_diff_key($defaults, $settings);
- if (!empty($missingSettings)) {
- $insertMissing = $db->prepare(
- 'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = setting_value'
- );
- foreach ($missingSettings as $missingKey => $missingValue) {
- $insertMissing->execute(['key' => $missingKey, 'value' => $missingValue]);
- }
- }
-
- $settings = array_merge($defaults, $settings);
-
- $featuredProductOptions = $db->query(
- "SELECT id, title, published_at FROM products WHERE status = 'published' ORDER BY published_at DESC, id DESC"
- )->fetchAll();
-
- Language::ensureKeys(Language::ADDED_DEFAULTS);
- $languageStrings = Language::base();
- $availableFonts = FontLibrary::available();
- $activeTab = $_GET['tab'] ?? 'general';
- $allowedTabs = ['general', 'seo', 'layout', 'navigation', 'search', 'reviews', 'email', 'footer', 'apps', 'language', 'custom-code', 'ai'];
- if (!in_array($activeTab, $allowedTabs, true)) {
- $activeTab = 'general';
- }
-
- $navItemsRaw = [];
- $navAvailableCategories = [];
- $navAvailablePages = [];
- if ($activeTab === 'navigation') {
- try {
- $navItemsRaw = $db->query('SELECT * FROM nav_items ORDER BY sort_order ASC, id ASC')->fetchAll();
- } catch (PDOException $e) {
- $navItemsRaw = [];
- }
-
- $allCategoryNames = [];
- foreach ($db->query('SELECT id, name FROM categories')->fetchAll() as $category) {
- $allCategoryNames[(int) $category['id']] = $category['name'];
- }
- $allPageTitles = [];
- foreach ($db->query('SELECT id, title FROM pages')->fetchAll() as $page) {
- $allPageTitles[(int) $page['id']] = $page['title'];
- }
-
- $usedCategoryIds = [];
- $usedPageIds = [];
- foreach ($navItemsRaw as $navRow) {
- if ($navRow['item_type'] === 'category') {
- $usedCategoryIds[] = (int) $navRow['ref_id'];
- }
- if ($navRow['item_type'] === 'page') {
- $usedPageIds[] = (int) $navRow['ref_id'];
- }
- }
-
- $allCategories = $db->query('SELECT id, name FROM categories ORDER BY name ASC')->fetchAll();
- foreach ($allCategories as $category) {
- if (!in_array((int) $category['id'], $usedCategoryIds, true)) {
- $navAvailableCategories[] = $category;
- }
- }
-
- $allPages = $db->query('SELECT id, title FROM pages WHERE is_home = 0 ORDER BY title ASC')->fetchAll();
- foreach ($allPages as $page) {
- if (!in_array((int) $page['id'], $usedPageIds, true)) {
- $navAvailablePages[] = $page;
- }
- }
- }
-
- $dashActivePage = 'settings';
- $dashPageTitle = 'Settings';
-
- require __DIR__ . '/includes/dash-header.php';
-
- ?>
-
- <?php if ($flashMessage !== null): ?>
- <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
- <?php endif; ?>
-
- <h1 class="dash-title"><?= Icons::icon("settings", "icon icon-lg") ?>Settings</h1>
-
- <div class="settings-tabs">
- <a href="settings.php?tab=general" class="<?= $activeTab === 'general' ? 'active' : '' ?>"><?= Icons::icon("sliders", "icon icon-sm") ?>General</a>
- <a href="settings.php?tab=seo" class="<?= $activeTab === 'seo' ? 'active' : '' ?>"><?= Icons::icon("robot", "icon icon-sm") ?>SEO</a>
- <a href="settings.php?tab=layout" class="<?= $activeTab === 'layout' ? 'active' : '' ?>"><?= Icons::icon("layout", "icon icon-sm") ?>Layout</a>
- <a href="settings.php?tab=navigation" class="<?= $activeTab === 'navigation' ? 'active' : '' ?>"><?= Icons::icon("compass", "icon icon-sm") ?>Navigation</a>
- <a href="settings.php?tab=search" class="<?= $activeTab === 'search' ? 'active' : '' ?>"><?= Icons::icon("search", "icon icon-sm") ?>Search</a>
- <a href="settings.php?tab=reviews" class="<?= $activeTab === 'reviews' ? 'active' : '' ?>"><?= Icons::icon("star", "icon icon-sm") ?>Reviews</a>
- <a href="settings.php?tab=email" class="<?= $activeTab === 'email' ? 'active' : '' ?>"><?= Icons::icon("mail", "icon icon-sm") ?>Email</a>
- <a href="settings.php?tab=footer" class="<?= $activeTab === 'footer' ? 'active' : '' ?>"><?= Icons::icon("footer", "icon icon-sm") ?>Footer</a>
- <a href="settings.php?tab=apps" class="<?= $activeTab === 'apps' ? 'active' : '' ?>"><?= Icons::icon("smartphone", "icon icon-sm") ?>Apps</a>
- <a href="settings.php?tab=language" class="<?= $activeTab === 'language' ? 'active' : '' ?>"><?= Icons::icon("translate", "icon icon-sm") ?>Language</a>
- <a href="settings.php?tab=custom-code" class="<?= $activeTab === 'custom-code' ? 'active' : '' ?>"><?= Icons::icon("code", "icon icon-sm") ?>Custom code</a>
- <a href="settings.php?tab=ai" class="<?= $activeTab === 'ai' ? 'active' : '' ?>"><?= Icons::icon("sparkles", "icon icon-sm") ?>AI</a>
- </div>
-
- <?php if ($activeTab === 'general'): ?>
- <div class="sidebar-box updater-card">
- <div class="updater-card-info">
- <?= Icons::icon('refresh', 'icon icon-lg') ?>
- <div>
- <div class="updater-card-title">Updates</div>
- <div class="updater-card-version"><?= htmlspecialchars(AppVersion::current($db)) ?></div>
- </div>
- </div>
- <a href="updater.php" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>Check for updates</a>
- </div>
-
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="general">
-
- <label>Store name</label>
- <input type="text" name="site_name" value="<?= htmlspecialchars($settings['site_name']) ?>">
-
- <label>Short description of your store</label>
- <textarea name="site_description" rows="2"><?= htmlspecialchars($settings['site_description']) ?></textarea>
-
- <label>Logo (optional, without it we show your store name as text)</label>
- <input type="text" name="site_logo_url" data-image-upload value="<?= htmlspecialchars($settings['site_logo_url']) ?>" placeholder="media/img/logo.png">
-
- <label><input type="checkbox" name="site_show_name_with_logo" <?= $settings['site_show_name_with_logo'] === '1' ? 'checked' : '' ?>> Show the store name next to the logo</label>
-
- <label>Short name</label>
- <input type="text" name="site_short_name" maxlength="30" value="<?= htmlspecialchars($settings['site_short_name']) ?>" placeholder="<?= htmlspecialchars($settings['site_name']) ?>">
-
- <label><input type="checkbox" name="site_short_name_enabled" <?= $settings['site_short_name_enabled'] === '1' ? 'checked' : '' ?>> Use the short name on small screens where the full name doesn't fit</label>
-
- <label><input type="checkbox" name="registration_enabled" <?= $settings['registration_enabled'] === '1' ? 'checked' : '' ?>> Let visitors create an account (existing customers can still log in if you turn this off)</label>
-
- <label><input type="checkbox" name="activity_log_enabled" <?= $settings['activity_log_enabled'] === '1' ? 'checked' : '' ?>> Keep an activity log, so you can see who changed products, categories, pages and settings</label>
-
- <label><input type="checkbox" name="login_enabled" <?= $settings['login_enabled'] === '1' ? 'checked' : '' ?>> Let customers log in (turn off to hide the login form, handy during maintenance)</label>
-
- <label><input type="checkbox" name="require_account_to_purchase" <?= $settings['require_account_to_purchase'] === '1' ? 'checked' : '' ?>> Customers need an account to buy or to download free files</label>
-
- <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>Digital downloads</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 12px;">Customers with an account can always download their purchases from their account page. The limits below only apply to the download links we email after a purchase.</p>
- <div class="settings-grid">
- <div>
- <label>Link works for this many days (0 means forever)</label>
- <input type="number" name="download_link_expiry_days" min="0" max="3650" value="<?= (int) $settings['download_link_expiry_days'] ?>">
- </div>
- <div>
- <label>Downloads per link (0 means no limit)</label>
- <input type="number" name="download_link_max_downloads" min="0" max="1000" value="<?= (int) $settings['download_link_max_downloads'] ?>">
- </div>
- </div>
-
- <h2 class="dash-subtitle"><?= Icons::icon('mail', 'icon icon-sm') ?>Contact form</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 12px;">Let visitors message you from any page. Turn it on, then add the "Contact form" block to a page in Pages. Each message has to pass a quick security check before it reaches you.</p>
- <label><input type="checkbox" name="contact_enabled" <?= $settings['contact_enabled'] === '1' ? 'checked' : '' ?>> Turn on the contact form</label>
-
- <label>Send messages to</label>
- <input type="email" name="contact_recipient_email" value="<?= htmlspecialchars($settings['contact_recipient_email']) ?>" placeholder="you@example.com">
-
- <label>Subject</label>
- <select name="contact_subject_mode" id="contact_subject_mode_select">
- <option value="free" <?= $settings['contact_subject_mode'] === 'free' ? 'selected' : '' ?>>Visitors type their own subject</option>
- <option value="list_other" <?= $settings['contact_subject_mode'] === 'list_other' ? 'selected' : '' ?>>Visitors pick a topic from the list, or choose "Other" and type their own</option>
- <option value="list_only" <?= $settings['contact_subject_mode'] === 'list_only' ? 'selected' : '' ?>>Visitors can only pick a topic from the list</option>
- </select>
- <?php
- require_once __DIR__ . '/includes/contact-form.php';
- $contactSubjectEntries = ContactForm::subjectEntries((string) $settings['contact_subjects']);
- ?>
- <div id="contact-subjects-field">
- <label>Topics (up to <?= ContactForm::MAX_SUBJECTS ?>)</label>
- <p style="font-size:12px;color:var(--muted);margin:0 0 8px;">Add an email to a topic if those messages should go to someone else. Topics without an email go to the address above.</p>
- <div class="contact-topic-list" id="contact-topic-list">
- <?php foreach ($contactSubjectEntries as $topic): ?>
- <div class="contact-topic-row">
- <input type="text" name="contact_subject_name[]" value="<?= htmlspecialchars($topic['name']) ?>" maxlength="100" placeholder="Topic name, e.g. Technical support">
- <input type="email" name="contact_subject_email[]" value="<?= htmlspecialchars($topic['email']) ?>" maxlength="190" placeholder="Email (optional)">
- <button type="button" class="dash-btn-small dash-btn-danger js-remove-topic" aria-label="Remove topic"><?= Icons::icon('trash', 'icon icon-sm') ?></button>
- </div>
- <?php endforeach; ?>
- </div>
- <button type="button" class="dash-btn-small" id="js-add-topic"><?= Icons::icon('plus', 'icon icon-sm') ?>Add topic</button>
- <p style="font-size:12px;color:var(--muted);margin:8px 0 0;">With no topics added, visitors always type their own subject.</p>
- <template id="contact-topic-template">
- <div class="contact-topic-row">
- <input type="text" name="contact_subject_name[]" maxlength="100" placeholder="Topic name, e.g. Technical support">
- <input type="email" name="contact_subject_email[]" maxlength="190" placeholder="Email (optional)">
- <button type="button" class="dash-btn-small dash-btn-danger js-remove-topic" aria-label="Remove topic"><?= Icons::icon('trash', 'icon icon-sm') ?></button>
- </div>
- </template>
- <script>
- (function() {
- var list = document.getElementById('contact-topic-list');
- var template = document.getElementById('contact-topic-template');
- var addButton = document.getElementById('js-add-topic');
- var max = <?= ContactForm::MAX_SUBJECTS ?>;
- function refresh() {
- addButton.disabled = list.querySelectorAll('.contact-topic-row').length >= max;
- }
- addButton.addEventListener('click', function() {
- if (list.querySelectorAll('.contact-topic-row').length >= max) {
- return;
- }
- list.appendChild(template.content.cloneNode(true));
- var inputs = list.querySelectorAll('.contact-topic-row input[type="text"]');
- inputs[inputs.length - 1].focus();
- refresh();
- });
- list.addEventListener('click', function(event) {
- var button = event.target.closest('.js-remove-topic');
- if (button) {
- button.closest('.contact-topic-row').remove();
- refresh();
- }
- });
- refresh();
- })();
- </script>
- </div>
-
- <label>How messages are sent</label>
- <select name="contact_provider" id="contact_provider_select">
- <option value="php_mail" <?= $settings['contact_provider'] === 'php_mail' ? 'selected' : '' ?>>Your hosting's built-in mail (works almost everywhere, nothing to set up)</option>
- <option value="resend" <?= $settings['contact_provider'] === 'resend' ? 'selected' : '' ?>>Resend (needs a Resend account)</option>
- <option value="postmark" <?= $settings['contact_provider'] === 'postmark' ? 'selected' : '' ?>>Postmark (needs a Postmark account)</option>
- <option value="store_mailer" <?= $settings['contact_provider'] === 'store_mailer' ? 'selected' : '' ?>>Same as your store emails in the Email tab (most reliable)</option>
- </select>
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">With your hosting's built-in mail, the sender address below should be a real mailbox on your domain, otherwise many hosts (Hostinger too) won't deliver. If nothing arrives, check your spam folder or switch to "Same as your store emails".</p>
-
- <label>Send from (optional)</label>
- <input type="email" name="contact_from_email" value="<?= htmlspecialchars($settings['contact_from_email']) ?>" placeholder="no-reply@yourdomain.com">
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">Use an address on your own domain. For Resend or Postmark, it has to be verified there first. When you hit reply, your answer goes straight to the visitor.</p>
-
- <div id="contact-resend-fields">
- <label>Resend API key (leave empty to reuse the one from the Email tab)</label>
- <input type="password" name="contact_resend_api_key" value="<?= htmlspecialchars($settings['contact_resend_api_key']) ?>" autocomplete="new-password" placeholder="re_...">
- </div>
- <div id="contact-postmark-fields">
- <label>Postmark server token (leave empty to reuse the one from the Email tab)</label>
- <input type="password" name="contact_postmark_token" value="<?= htmlspecialchars($settings['contact_postmark_token']) ?>" autocomplete="new-password">
- </div>
- <script>
- (function() {
- var select = document.getElementById('contact_provider_select');
- var resend = document.getElementById('contact-resend-fields');
- var postmark = document.getElementById('contact-postmark-fields');
- var subjectMode = document.getElementById('contact_subject_mode_select');
- var subjectsField = document.getElementById('contact-subjects-field');
- function apply() {
- resend.style.display = select.value === 'resend' ? '' : 'none';
- postmark.style.display = select.value === 'postmark' ? '' : 'none';
- subjectsField.style.display = subjectMode.value === 'free' ? 'none' : '';
- }
- select.addEventListener('change', apply);
- subjectMode.addEventListener('change', apply);
- apply();
- })();
- </script>
-
- <h2 class="dash-subtitle"><?= Icons::icon('credit-card', 'icon icon-sm') ?>Store type</h2>
-
- <label>What do you sell?</label>
- <select name="store_category" id="store_category_select">
- <option value="general" <?= $settings['store_category'] === 'general' ? 'selected' : '' ?>>Physical or digital products</option>
- <option value="saas" <?= $settings['store_category'] === 'saas' ? 'selected' : '' ?>>Software as a service (SaaS)</option>
- </select>
-
- <label>How customers pay</label>
- <select name="pricing_model">
- <option value="per_product" <?= $settings['pricing_model'] === 'per_product' ? 'selected' : '' ?>>Each product has its own price</option>
- <option value="subscription" <?= $settings['pricing_model'] === 'subscription' ? 'selected' : '' ?>>Subscriptions</option>
- </select>
-
- <label>Default billing period for new subscriptions</label>
- <select name="subscription_interval">
- <option value="month" <?= $settings['subscription_interval'] === 'month' ? 'selected' : '' ?>>Monthly</option>
- <option value="year" <?= $settings['subscription_interval'] === 'year' ? 'selected' : '' ?>>Yearly</option>
- <option value="lifetime" <?= $settings['subscription_interval'] === 'lifetime' ? 'selected' : '' ?>>Pay once, keep forever</option>
- </select>
-
- <label>Currency (used for new products, type a code or name like PLN, zloty or dollar)</label>
- <?php $storeCurrency = Currency::normalize((string) $settings['store_currency']); ?>
- <input type="text" name="store_currency" id="store-currency" list="store-currency-list" value="<?= htmlspecialchars($storeCurrency) ?>" maxlength="40" autocomplete="off" spellcheck="false" style="text-transform:uppercase;">
- <datalist id="store-currency-list">
- <?php foreach (Currency::all() as $currencyCode => $currencyName): ?>
- <option value="<?= htmlspecialchars($currencyCode) ?>"><?= htmlspecialchars(Currency::searchLabel($currencyCode)) ?></option>
- <?php endforeach; ?>
- </datalist>
- <script>
- (function() {
- var map = <?= json_encode(Currency::clientMap(), JSON_UNESCAPED_UNICODE) ?>;
- var currency = document.getElementById('store-currency');
- var lastValid = currency.value;
-
- currency.addEventListener('blur', function() {
- var code = currency.value.trim().toUpperCase();
- currency.value = map[code] ? code : lastValid;
- lastValid = currency.value;
- });
- })();
- </script>
-
- <label>Payment provider</label>
- <select name="payment_provider" id="payment_provider_select">
- <option value="stripe" <?= $settings['payment_provider'] === 'stripe' ? 'selected' : '' ?>>Stripe</option>
- <option value="polar" id="payment_provider_polar_option" <?= $settings['payment_provider'] === 'polar' ? 'selected' : '' ?> <?= $settings['store_category'] !== 'saas' ? 'disabled' : '' ?>>Polar.sh<?= $settings['store_category'] !== 'saas' ? ' (only for SaaS stores)' : '' ?></option>
- </select>
-
- <div id="stripe-payment-fields">
- <label>Stripe secret key</label>
- <input type="password" name="payment_stripe_secret_key" value="<?= htmlspecialchars($settings['payment_stripe_secret_key']) ?>" autocomplete="new-password" placeholder="sk_live_...">
-
- <label>Stripe publishable key</label>
- <input type="text" name="payment_stripe_publishable_key" value="<?= htmlspecialchars($settings['payment_stripe_publishable_key']) ?>" placeholder="pk_live_...">
- </div>
-
- <div id="polar-payment-fields">
- <label>Polar.sh access token</label>
- <input type="password" name="payment_polar_access_token" value="<?= htmlspecialchars($settings['payment_polar_access_token']) ?>" autocomplete="new-password">
-
- <label>Polar.sh organization ID</label>
- <input type="text" name="payment_polar_organization_id" value="<?= htmlspecialchars($settings['payment_polar_organization_id']) ?>">
-
- <label>Polar.sh webhook secret</label>
- <input type="password" name="payment_polar_webhook_secret" value="<?= htmlspecialchars($settings['payment_polar_webhook_secret']) ?>" autocomplete="new-password">
- </div>
-
- <script>
- (function() {
- var storeCategorySelect = document.getElementById('store_category_select');
- var paymentProviderSelect = document.getElementById('payment_provider_select');
- var polarOption = document.getElementById('payment_provider_polar_option');
- var stripeFields = document.getElementById('stripe-payment-fields');
- var polarFields = document.getElementById('polar-payment-fields');
-
- function applyPaymentProvider() {
- stripeFields.style.display = '';
- polarFields.style.display = storeCategorySelect.value === 'saas' ? '' : 'none';
- }
-
- function applyStoreCategory() {
- var isSaas = storeCategorySelect.value === 'saas';
- polarOption.disabled = !isSaas;
- polarOption.textContent = isSaas ? 'Polar.sh' : 'Polar.sh (only for SaaS stores)';
- if (!isSaas && paymentProviderSelect.value === 'polar') {
- paymentProviderSelect.value = 'stripe';
- }
- applyPaymentProvider();
- }
-
- storeCategorySelect.addEventListener('change', applyStoreCategory);
- paymentProviderSelect.addEventListener('change', applyPaymentProvider);
- applyStoreCategory();
- })();
- </script>
-
- <label>Stripe webhook secret (starts with whsec_)</label>
- <input type="password" name="payment_webhook_secret" value="<?= htmlspecialchars($settings['payment_webhook_secret']) ?>" autocomplete="new-password" placeholder="whsec_...">
- <p style="font-size:12px;color:var(--muted);margin-top:6px;">In Stripe, send webhooks to: <code><?= htmlspecialchars(rtrim(Config::get('APP_URL', ''), '/') . Config::get('APP_BASE_PATH', '')) ?>/webhook.php</code></p>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'seo'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="seo">
-
- <label>Title shown in Google</label>
- <input type="text" name="seo_title" value="<?= htmlspecialchars($settings['seo_title']) ?>">
-
- <label>Description shown in Google</label>
- <textarea name="seo_description" rows="2"><?= htmlspecialchars($settings['seo_description']) ?></textarea>
-
- <label>Keywords (separate with commas)</label>
- <input type="text" name="seo_keywords" value="<?= htmlspecialchars($settings['seo_keywords']) ?>" placeholder="news, technology, business">
-
- <label>Author</label>
- <input type="text" name="seo_author" value="<?= htmlspecialchars($settings['seo_author']) ?>">
-
- <label>Contact email</label>
- <input type="email" name="seo_reply_to" value="<?= htmlspecialchars($settings['seo_reply_to']) ?>">
-
- <label>App name (shown when someone adds your store to their home screen)</label>
- <input type="text" name="seo_application_name" value="<?= htmlspecialchars($settings['seo_application_name']) ?>">
-
- <h2 class="dash-subtitle"><?= Icons::icon('robot', 'icon icon-sm') ?>Search engines</h2>
- <label><input type="checkbox" name="seo_robots_index" <?= $settings['seo_robots_index'] === '1' ? 'checked' : '' ?>> Let Google and other search engines show your store</label>
- <label><input type="checkbox" name="seo_robots_follow" <?= $settings['seo_robots_follow'] === '1' ? 'checked' : '' ?>> Let search engines follow links on your store</label>
-
- <h2 class="dash-subtitle"><?= Icons::icon('share', 'icon icon-sm') ?>Sharing on social media</h2>
- <label><input type="checkbox" name="seo_og_product_image_enabled" <?= $settings['seo_og_product_image_enabled'] === '1' ? 'checked' : '' ?>> Use the product photo when someone shares a product (otherwise we use your logo)</label>
- <label><input type="checkbox" name="clean_product_urls" <?= $settings['clean_product_urls'] === '1' ? 'checked' : '' ?>> Use shorter, cleaner product links</label>
- <label><input type="checkbox" name="seo_preview_enabled" <?= $settings['seo_preview_enabled'] === '1' ? 'checked' : '' ?>> Show a search and social preview while editing a product</label>
- <label><input type="checkbox" name="indexnow_enabled" <?= $settings['indexnow_enabled'] === '1' ? 'checked' : '' ?>> Tell search engines right away when you add, change or remove a product (IndexNow)</label>
- <?php if ($settings['indexnow_enabled'] === '1' && $settings['indexnow_key'] !== ''): ?>
- <p style="font-size:12px;color:var(--muted);margin-top:6px;">Key: <code><?= htmlspecialchars($settings['indexnow_key']) ?></code> · check file: <code><?= htmlspecialchars(rtrim(Config::get('APP_URL', ''), '/') . rtrim(Config::get('APP_BASE_PATH', ''), '/') . '/' . $settings['indexnow_key'] . '.txt') ?></code></p>
- <?php endif; ?>
- <label><input type="checkbox" name="llms_txt_enabled" <?= $settings['llms_txt_enabled'] === '1' ? 'checked' : '' ?>> Publish a short summary of your store for AI assistants (/llms.txt)</label>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'layout'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="layout">
-
- <label>Where the menu sits</label>
- <select name="menu_position">
- <option value="top" <?= $settings['menu_position'] === 'top' ? 'selected' : '' ?>>Top</option>
- <option value="bottom" <?= $settings['menu_position'] === 'bottom' ? 'selected' : '' ?>>Bottom</option>
- <option value="side" <?= $settings['menu_position'] === 'side' ? 'selected' : '' ?>>Side</option>
- </select>
-
- <label><input type="checkbox" name="featured_banner_enabled" <?= $settings['featured_banner_enabled'] === '1' ? 'checked' : '' ?>> Show featured products on the home page</label>
-
- <h2 class="dash-subtitle"><?= Icons::icon('sparkles', 'icon icon-sm') ?>Featured products</h2>
- <label>Which products to feature</label>
- <select name="featured_banner_mode" id="featured_banner_mode_select">
- <option value="static" <?= $settings['featured_banner_mode'] === 'static' ? 'selected' : '' ?>>I'll pick them myself</option>
- <option value="automatic" <?= $settings['featured_banner_mode'] === 'automatic' ? 'selected' : '' ?>>Pick automatically (most viewed lately)</option>
- </select>
-
- <label><input type="checkbox" name="featured_banner_hide_duplicates" <?= $settings['featured_banner_hide_duplicates'] === '1' ? 'checked' : '' ?>> Don't repeat featured products in the product list below</label>
-
- <div id="featured-banner-static-fields">
- <div class="settings-grid">
- <?php for ($featuredSlot = 1; $featuredSlot <= 5; $featuredSlot++): ?>
- <div>
- <label>Featured product <?= $featuredSlot ?></label>
- <select name="featured_banner_product_<?= $featuredSlot ?>">
- <option value="0">None</option>
- <?php foreach ($featuredProductOptions as $featuredOption): ?>
- <option value="<?= (int) $featuredOption['id'] ?>" <?= (string) $settings['featured_banner_product_' . $featuredSlot] === (string) $featuredOption['id'] ? 'selected' : '' ?>><?= htmlspecialchars($featuredOption['title']) ?></option>
- <?php endforeach; ?>
- </select>
- </div>
- <?php endfor; ?>
- </div>
- </div>
-
- <div id="featured-banner-auto-fields">
- <label>Look at products from the last</label>
- <div class="settings-grid">
- <input type="number" name="featured_banner_auto_window_value" min="1" max="720" value="<?= (int) $settings['featured_banner_auto_window_value'] ?>">
- <select name="featured_banner_auto_window_unit">
- <option value="minutes" <?= $settings['featured_banner_auto_window_unit'] === 'minutes' ? 'selected' : '' ?>>Minutes</option>
- <option value="hours" <?= $settings['featured_banner_auto_window_unit'] === 'hours' ? 'selected' : '' ?>>Hours</option>
- </select>
- </div>
-
- <label>Mixing categories</label>
- <select name="featured_banner_auto_category_mode">
- <option value="mixed" <?= $settings['featured_banner_auto_category_mode'] === 'mixed' ? 'selected' : '' ?>>Mix categories evenly</option>
- <option value="one_per_category" <?= $settings['featured_banner_auto_category_mode'] === 'one_per_category' ? 'selected' : '' ?>>One product per category</option>
- <option value="no_limit" <?= $settings['featured_banner_auto_category_mode'] === 'no_limit' ? 'selected' : '' ?>>Don't care about categories</option>
- </select>
- </div>
-
- <script>
- (function() {
- var modeSelect = document.getElementById('featured_banner_mode_select');
- var staticFields = document.getElementById('featured-banner-static-fields');
- var autoFields = document.getElementById('featured-banner-auto-fields');
-
- function apply() {
- var isAutomatic = modeSelect.value === 'automatic';
- staticFields.style.display = isAutomatic ? 'none' : '';
- autoFields.style.display = isAutomatic ? '' : 'none';
- }
-
- modeSelect.addEventListener('change', apply);
- apply();
- })();
- </script>
-
- <label><input type="checkbox" name="back_to_top_enabled" <?= $settings['back_to_top_enabled'] === '1' ? 'checked' : '' ?>> Show a "Back to top" button after scrolling down</label>
- <label><input type="checkbox" name="back_to_top_progress_enabled" <?= $settings['back_to_top_progress_enabled'] === '1' ? 'checked' : '' ?>> Show scroll progress (%) inside the "Back to top" button</label>
- <label><input type="checkbox" name="breadcrumbs_enabled" <?= $settings['breadcrumbs_enabled'] === '1' ? 'checked' : '' ?>> Show the path above the product title (Home › Category › Product)</label>
- <label><input type="checkbox" name="tags_enabled" <?= $settings['tags_enabled'] === '1' ? 'checked' : '' ?>> Use tags on products (customers can click a tag to see similar products)</label>
- <label><input type="checkbox" name="related_products_enabled" <?= $settings['related_products_enabled'] === '1' ? 'checked' : '' ?>> Show other products under each product</label>
- <label><input type="checkbox" name="product_show_buyers" <?= $settings['product_show_buyers'] === '1' ? 'checked' : '' ?>> Show how many people bought a product (hidden until the first sale)</label>
- <label><input type="checkbox" name="product_show_downloads" <?= $settings['product_show_downloads'] === '1' ? 'checked' : '' ?>> Show how many times a product was downloaded (hidden until the first download)</label>
- <label><input type="checkbox" name="sitemap_rss_enabled" <?= $settings['sitemap_rss_enabled'] === '1' ? 'checked' : '' ?>> Create a sitemap and RSS feed (helps search engines find your products)</label>
-
- <h2 class="dash-subtitle"><?= Icons::icon('more', 'icon icon-sm') ?>Product list</h2>
- <label>How products load</label>
- <select name="product_loading_mode">
- <option value="scroll" <?= $settings['product_loading_mode'] === 'scroll' ? 'selected' : '' ?>>A few at a time</option>
- <option value="static" <?= $settings['product_loading_mode'] === 'static' ? 'selected' : '' ?>>All at once</option>
- </select>
- <label>Showing more products</label>
- <select name="pagination_style">
- <option value="numbered" <?= $settings['pagination_style'] === 'numbered' ? 'selected' : '' ?>>Page numbers (1 2 3 … Next)</option>
- <option value="load_more" <?= $settings['pagination_style'] === 'load_more' ? 'selected' : '' ?>>"Load more" button</option>
- <option value="infinite_scroll" <?= $settings['pagination_style'] === 'infinite_scroll' ? 'selected' : '' ?>>Load more while scrolling</option>
- </select>
-
- <label>Products per page on the home page</label>
- <input type="number" name="products_per_page_home" min="1" max="100" value="<?= (int) $settings['products_per_page_home'] ?>">
-
- <label>Products per page in categories</label>
- <input type="number" name="products_per_page_category" min="1" max="100" value="<?= (int) $settings['products_per_page_category'] ?>">
-
- <label>Font for headings</label>
- <select name="heading_font">
- <?php if (empty($availableFonts)): ?>
- <option value="<?= htmlspecialchars($settings['heading_font']) ?>"><?= htmlspecialchars($settings['heading_font']) ?> (no other fonts found)</option>
- <?php endif; ?>
- <?php foreach ($availableFonts as $font): ?>
- <option value="<?= htmlspecialchars($font['display_name']) ?>" <?= $settings['heading_font'] === $font['display_name'] ? 'selected' : '' ?>><?= htmlspecialchars($font['display_name']) ?></option>
- <?php endforeach; ?>
- </select>
-
- <label>Font for text</label>
- <select name="body_font">
- <?php if (empty($availableFonts)): ?>
- <option value="<?= htmlspecialchars($settings['body_font']) ?>"><?= htmlspecialchars($settings['body_font']) ?> (no other fonts found)</option>
- <?php endif; ?>
- <?php foreach ($availableFonts as $font): ?>
- <option value="<?= htmlspecialchars($font['display_name']) ?>" <?= $settings['body_font'] === $font['display_name'] ? 'selected' : '' ?>><?= htmlspecialchars($font['display_name']) ?></option>
- <?php endforeach; ?>
- </select>
-
- <label>Font for menus and buttons</label>
- <select name="interface_font">
- <?php if (empty($availableFonts)): ?>
- <option value="<?= htmlspecialchars($settings['interface_font']) ?>"><?= htmlspecialchars($settings['interface_font']) ?> (no other fonts found)</option>
- <?php endif; ?>
- <?php foreach ($availableFonts as $font): ?>
- <option value="<?= htmlspecialchars($font['display_name']) ?>" <?= $settings['interface_font'] === $font['display_name'] ? 'selected' : '' ?>><?= htmlspecialchars($font['display_name']) ?></option>
- <?php endforeach; ?>
- </select>
-
- <h2 class="dash-subtitle"><?= Icons::icon('share', 'icon icon-sm') ?>Share on social media</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 16px;">Adds a Share button to products so customers can send them to friends.</p>
- <label><input type="checkbox" name="share_enabled" <?= $settings['share_enabled'] === '1' ? 'checked' : '' ?>> Show the Share button</label>
-
- <div class="settings-grid">
- <label class="share-settings-platform"><input type="checkbox" name="share_facebook" <?= $settings['share_facebook'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><?= SocialIcons::builtinSvg('facebook') ?></span> Facebook</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_twitter" <?= $settings['share_twitter'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><?= SocialIcons::builtinSvg('twitter') ?></span> Twitter / X</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_linkedin" <?= $settings['share_linkedin'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><?= SocialIcons::builtinSvg('linkedin') ?></span> LinkedIn</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_whatsapp" <?= $settings['share_whatsapp'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><svg viewBox="0 0 24 24"><path d="M12.04 2c-5.52 0-10 4.48-10 10 0 1.77.46 3.5 1.34 5.02L2 22l5.13-1.35a9.96 9.96 0 0 0 4.91 1.29h.01c5.52 0 10-4.48 10-10S17.56 2 12.04 2zm5.87 14.24c-.25.7-1.45 1.34-2 1.42-.51.08-1.16.12-1.87-.12-.43-.14-.98-.32-1.69-.63-2.97-1.28-4.9-4.27-5.05-4.47-.15-.2-1.21-1.61-1.21-3.07s.76-2.18 1.03-2.48c.27-.3.59-.37.79-.37.2 0 .4 0 .57.01.18.01.43-.07.67.51.25.6.85 2.08.92 2.23.07.15.12.33.02.53-.1.2-.15.33-.3.5-.15.18-.31.4-.45.54-.15.15-.3.31-.13.61.17.3.77 1.27 1.65 2.06 1.13 1.01 2.09 1.32 2.39 1.47.3.15.48.13.65-.08.18-.2.75-.87.95-1.17.2-.3.4-.25.67-.15.27.1 1.73.82 2.03.97.3.15.5.22.57.35.08.13.08.73-.17 1.43z"/></svg></span> WhatsApp</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_telegram" <?= $settings['share_telegram'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><svg viewBox="0 0 24 24"><path d="M21.9 4.3 18.8 19.5c-.23 1.05-.85 1.3-1.72.81l-4.75-3.5-2.29 2.2c-.25.25-.47.47-.96.47l.34-4.85 8.8-7.95c.38-.34-.09-.53-.6-.19L6.9 12.6l-4.7-1.47c-1.02-.32-1.04-1.02.21-1.5L20.6 2.9c.85-.32 1.6.2 1.3 1.4z"/></svg></span> Telegram</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_email" <?= $settings['share_email'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M3 7l9 6 9-6"></path></svg></span> Email</label>
- <label class="share-settings-platform"><input type="checkbox" name="share_copy_link" <?= $settings['share_copy_link'] === '1' ? 'checked' : '' ?>> <span class="share-settings-platform-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07l-1.42 1.41"></path><path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07l1.41-1.41"></path></svg></span> Copy link</label>
- </div>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'navigation'): ?>
- <?php
- function renderNavIconPickerFields(string $selectedKey = '', string $selectedSvg = ''): void
- {
- $previewKey = Icons::isNavIcon($selectedKey) ? $selectedKey : 'home';
- ?>
- <label>Icon (optional)</label>
- <div class="nav-icon-picker-row">
- <span class="nav-icon-picker-preview"><?= Icons::icon($previewKey, 'icon js-nav-icon-preview') ?></span>
- <select name="icon_key" class="js-nav-icon-select">
- <option value="" <?= $selectedKey === '' ? 'selected' : '' ?>>No icon</option>
- <?php foreach (Icons::navIconKeys() as $navIconKeyOption => $navIconLabel): ?>
- <option value="<?= htmlspecialchars($navIconKeyOption) ?>" <?= $selectedKey === $navIconKeyOption ? 'selected' : '' ?>><?= htmlspecialchars($navIconLabel) ?></option>
- <?php endforeach; ?>
- <option value="custom" <?= $selectedKey === 'custom' ? 'selected' : '' ?>>Custom SVG…</option>
- </select>
- </div>
- <div class="js-nav-icon-custom-wrap" style="<?= $selectedKey === 'custom' ? '' : 'display:none;' ?>">
- <textarea name="icon_svg" class="be-code js-nav-icon-svg" rows="2" placeholder='<svg viewBox="0 0 24 24">...</svg>'><?= htmlspecialchars($selectedSvg) ?></textarea>
- </div>
- <?php
- }
- ?>
- <?php
- $shopIconPresets = [
- 'cart' => 'Basket',
- 'cart-basket2' => 'Basket (straps)',
- 'cart-trolley' => 'Trolley',
- 'cart-bag' => 'Shopping bag',
- 'cart-tote' => 'Tote bag',
- 'cart-box' => 'Package',
- 'cart-wallet' => 'Wallet',
- 'cart-tag' => 'Price tag',
- 'cart-storefront' => 'Storefront',
- 'cart-percent' => 'Discount',
- ];
- $shopIconChoice = (string) ($settings['shop_icon_choice'] ?? 'cart');
- if (!isset($shopIconPresets[$shopIconChoice]) && $shopIconChoice !== 'custom') {
- $shopIconChoice = 'cart';
- }
- ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="shop_icon">
-
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('cart', 'icon icon-sm') ?>Shop icon</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 12px;">The icon next to the cart at the top of your store. Pick one or paste your own SVG.</p>
- <div class="shop-icon-picker">
- <?php foreach ($shopIconPresets as $presetKey => $presetLabel): ?>
- <label class="shop-icon-option" title="<?= htmlspecialchars($presetLabel) ?>">
- <input type="radio" name="shop_icon_choice" value="<?= htmlspecialchars($presetKey) ?>" <?= $shopIconChoice === $presetKey ? 'checked' : '' ?>>
- <?= Icons::icon($presetKey, 'icon') ?>
- </label>
- <?php endforeach; ?>
- <label class="shop-icon-option shop-icon-option-custom" title="Custom SVG">
- <input type="radio" name="shop_icon_choice" value="custom" <?= $shopIconChoice === 'custom' ? 'checked' : '' ?>>
- <?= Icons::icon('code', 'icon') ?>
- </label>
- </div>
- <div class="shop-icon-custom-wrap">
- <label>Your SVG code</label>
- <textarea name="shop_icon_custom_svg" class="be-code" rows="3" placeholder='<svg viewBox="0 0 24 24">...</svg>'><?= htmlspecialchars((string) ($settings['shop_icon_custom_svg'] ?? '')) ?></textarea>
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">It goes on your site exactly as pasted, so only use an <svg> from a source you trust.</p>
- </div>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save shop icon</button>
- </form>
-
- <div class="settings-section">
- <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Choose what shows up in your store's menu and in what order. While this list is empty, the menu simply shows every category and page marked "Show in menu".</p>
-
- <?php if (empty($navItemsRaw)): ?>
- <p style="color:var(--muted);font-size:13.5px;">You haven't built a menu yet, so your store shows categories and pages automatically. Add something below to set it up your way.</p>
- <?php else: ?>
- <table class="dash-table">
- <thead>
- <tr>
- <th></th>
- <th><?= Icons::icon('heading', 'icon icon-sm') ?>Label</th>
- <th><?= Icons::icon('tag', 'icon icon-sm') ?>Type</th>
- <th><?= Icons::icon('link', 'icon icon-sm') ?>Opens in</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ($navItemsRaw as $navIndex => $navRow): ?>
- <?php
- if ($navRow['item_type'] === 'category') {
- $navDisplayLabel = $navRow['label'] !== null && $navRow['label'] !== '' ? $navRow['label'] : ($allCategoryNames[(int) $navRow['ref_id']] ?? '(deleted category)');
- $navTypeLabel = 'Category';
- } elseif ($navRow['item_type'] === 'page') {
- $navDisplayLabel = $navRow['label'] !== null && $navRow['label'] !== '' ? $navRow['label'] : ($allPageTitles[(int) $navRow['ref_id']] ?? '(deleted page)');
- $navTypeLabel = 'Page';
- } else {
- $navDisplayLabel = $navRow['label'] ?? '';
- $navTypeLabel = 'Custom link';
- }
- $navRowIconKey = (string) ($navRow['icon_key'] ?? '');
- $navRowIconSvg = (string) ($navRow['icon_svg'] ?? '');
- ?>
- <tr>
- <td>
- <?php if ($navRowIconKey === 'custom' && $navRowIconSvg !== ''): ?>
- <span class="nav-item-icon" aria-hidden="true"><?= $navRowIconSvg ?></span>
- <?php elseif ($navRowIconKey !== '' && Icons::isNavIcon($navRowIconKey)): ?>
- <?= Icons::icon($navRowIconKey, 'icon icon-sm nav-item-icon') ?>
- <?php else: ?>
- <span style="color:var(--muted);">None</span>
- <?php endif; ?>
- </td>
- <td><?= htmlspecialchars($navDisplayLabel) ?><?php if ($navRow['item_type'] === 'custom'): ?><div style="font-size:12px;color:var(--muted);"><?= htmlspecialchars($navRow['url']) ?></div><?php endif; ?></td>
- <td><?= htmlspecialchars($navTypeLabel) ?></td>
- <td><?= $navRow['target'] === '_blank' ? 'New tab' : 'Same tab' ?></td>
- <td>
- <div class="dash-table-actions">
- <?php if ($navIndex > 0): ?>
- <form method="post" style="display:inline;">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="move_nav_item">
- <input type="hidden" name="nav_item_id" value="<?= (int) $navRow['id'] ?>">
- <input type="hidden" name="direction" value="up">
- <button type="submit" class="dash-btn-small" aria-label="Move up"><?= Icons::icon('arrow-up', 'icon icon-sm') ?></button>
- </form>
- <?php endif; ?>
- <?php if ($navIndex < count($navItemsRaw) - 1): ?>
- <form method="post" style="display:inline;">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="move_nav_item">
- <input type="hidden" name="nav_item_id" value="<?= (int) $navRow['id'] ?>">
- <input type="hidden" name="direction" value="down">
- <button type="submit" class="dash-btn-small" aria-label="Move down"><?= Icons::icon('arrow-down', 'icon icon-sm') ?></button>
- </form>
- <?php endif; ?>
- <button type="button" class="dash-btn-small js-edit-nav-icon"
- data-id="<?= (int) $navRow['id'] ?>"
- data-icon-key="<?= htmlspecialchars($navRowIconKey, ENT_QUOTES) ?>"
- data-icon-svg="<?= htmlspecialchars($navRowIconSvg, ENT_QUOTES) ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Icon</button>
- <form method="post" style="display:inline;" onsubmit="return confirm('Remove this menu item?');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="delete_nav_item">
- <input type="hidden" name="nav_item_id" value="<?= (int) $navRow['id'] ?>">
- <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Remove</button>
- </form>
- </div>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php endif; ?>
- </div>
-
- <div class="sidebar-box" style="max-width:520px;margin-top:20px;display:none;" id="nav-icon-form-wrapper">
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('star', 'icon icon-sm') ?>Icon for this menu item</h2>
- <form method="post" id="nav-icon-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="update_nav_item_icon">
- <input type="hidden" name="nav_item_id" id="nav_icon_item_id_input" value="">
- <?php renderNavIconPickerFields(); ?>
- <div class="publish-actions" style="margin-top:12px;">
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save icon</button>
- <button type="button" class="dash-btn" id="nav-icon-cancel-btn"><?= Icons::icon('x', 'icon icon-sm') ?>Cancel</button>
- </div>
- </form>
- </div>
-
- <div class="sidebar-box" style="max-width:520px;margin-top:20px;">
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add menu item</h2>
-
- <div class="settings-tabs" id="nav-add-type-tabs" style="margin-bottom:16px;">
- <a href="#" class="active" data-nav-type="category"><?= Icons::icon("categories", "icon icon-sm") ?>Category</a>
- <a href="#" data-nav-type="page"><?= Icons::icon("pages", "icon icon-sm") ?>Page</a>
- <a href="#" data-nav-type="custom"><?= Icons::icon("link", "icon icon-sm") ?>Custom link</a>
- </div>
-
- <form method="post" id="nav-add-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="add_nav_item">
- <input type="hidden" name="item_type" id="nav_item_type_input" value="category">
-
- <div id="nav-add-category-fields">
- <label>Category</label>
- <select name="category_ref_id">
- <?php if (empty($navAvailableCategories)): ?>
- <option value="">No categories yet</option>
- <?php endif; ?>
- <?php foreach ($navAvailableCategories as $navCategoryOption): ?>
- <option value="<?= (int) $navCategoryOption['id'] ?>"><?= htmlspecialchars($navCategoryOption['name']) ?></option>
- <?php endforeach; ?>
- </select>
- </div>
-
- <div id="nav-add-page-fields" style="display:none;">
- <label>Page</label>
- <select name="page_ref_id">
- <?php if (empty($navAvailablePages)): ?>
- <option value="">No pages yet</option>
- <?php endif; ?>
- <?php foreach ($navAvailablePages as $navPageOption): ?>
- <option value="<?= (int) $navPageOption['id'] ?>"><?= htmlspecialchars($navPageOption['title']) ?></option>
- <?php endforeach; ?>
- </select>
- </div>
-
- <div id="nav-add-custom-fields" style="display:none;">
- <label>Label</label>
- <input type="text" name="custom_label">
-
- <label>Link</label>
- <input type="text" name="custom_url" placeholder="https://example.com or a relative page">
-
- <label><input type="checkbox" name="custom_new_tab"> Open in a new tab</label>
- </div>
-
- <label id="nav-label-override-wrap" style="display:none;">Custom label (optional, leave empty to use the name)</label>
- <input type="text" name="label_override" id="nav_label_override_input" style="display:none;">
-
- <?php renderNavIconPickerFields(); ?>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add to menu</button>
- </form>
- </div>
-
- <script>
- (function() {
- function wireIconPicker(form) {
- var select = form.querySelector('.js-nav-icon-select');
- var preview = form.querySelector('.js-nav-icon-preview use');
- var customWrap = form.querySelector('.js-nav-icon-custom-wrap');
-
- function apply() {
- var isCustom = select.value === 'custom';
- customWrap.style.display = isCustom ? '' : 'none';
- if (!isCustom && select.value !== '') {
- preview.setAttribute('href', '#icon-' + select.value);
- }
- form.querySelector('.js-nav-icon-preview').style.visibility = (select.value === '' || isCustom) ? 'hidden' : 'visible';
- }
-
- select.addEventListener('change', apply);
- apply();
- }
-
- document.querySelectorAll('#nav-icon-form, #nav-add-form').forEach(wireIconPicker);
-
- var iconWrapper = document.getElementById('nav-icon-form-wrapper');
- var iconForm = document.getElementById('nav-icon-form');
-
- document.querySelectorAll('.js-edit-nav-icon').forEach(function(button) {
- button.addEventListener('click', function() {
- document.getElementById('nav_icon_item_id_input').value = this.dataset.id;
- var select = iconForm.querySelector('.js-nav-icon-select');
- var svgTextarea = iconForm.querySelector('.js-nav-icon-svg');
- select.value = this.dataset.iconKey || '';
- svgTextarea.value = this.dataset.iconSvg || '';
- select.dispatchEvent(new Event('change'));
- iconWrapper.style.display = '';
- iconWrapper.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest'
- });
- });
- });
-
- document.getElementById('nav-icon-cancel-btn').addEventListener('click', function() {
- iconWrapper.style.display = 'none';
- });
- })();
- </script>
-
- <script>
- (function() {
- var typeTabs = document.querySelectorAll('#nav-add-type-tabs a');
- var typeInput = document.getElementById('nav_item_type_input');
- var categoryFields = document.getElementById('nav-add-category-fields');
- var pageFields = document.getElementById('nav-add-page-fields');
- var customFields = document.getElementById('nav-add-custom-fields');
- var labelOverrideWrap = document.getElementById('nav-label-override-wrap');
- var labelOverrideInput = document.getElementById('nav_label_override_input');
-
- function setType(type) {
- typeInput.value = type;
- categoryFields.style.display = type === 'category' ? '' : 'none';
- pageFields.style.display = type === 'page' ? '' : 'none';
- customFields.style.display = type === 'custom' ? '' : 'none';
- var showOverride = type === 'category' || type === 'page';
- labelOverrideWrap.style.display = showOverride ? '' : 'none';
- labelOverrideInput.style.display = showOverride ? '' : 'none';
-
- typeTabs.forEach(function(tab) {
- tab.classList.toggle('active', tab.dataset.navType === type);
- });
- }
-
- typeTabs.forEach(function(tab) {
- tab.addEventListener('click', function(event) {
- event.preventDefault();
- setType(this.dataset.navType);
- });
- });
- })();
- </script>
- <?php endif; ?>
-
- <?php if ($activeTab === 'search'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="search">
-
- <label><input type="checkbox" name="search_enabled" <?= $settings['search_enabled'] === '1' ? 'checked' : '' ?>> Let customers search your store</label>
- <p style="font-size:12px;color:var(--muted);margin:-6px 0 16px;">When this is off, the search box disappears everywhere and the search page is closed.</p>
-
- <h2 class="dash-subtitle"><?= Icons::icon('search', 'icon icon-sm') ?>Search in</h2>
- <label><input type="checkbox" name="search_match_title" <?= $settings['search_match_title'] === '1' ? 'checked' : '' ?>> Product titles</label>
- <label><input type="checkbox" name="search_match_excerpt" <?= $settings['search_match_excerpt'] === '1' ? 'checked' : '' ?>> Short product summaries</label>
- <label><input type="checkbox" name="search_match_content" <?= $settings['search_match_content'] === '1' ? 'checked' : '' ?>> Full product descriptions</label>
- <p style="font-size:12px;color:var(--muted);margin:-6px 0 16px;">Keep at least one of these ticked, or search won't find anything.</p>
-
- <h2 class="dash-subtitle"><?= Icons::icon('palette', 'icon icon-sm') ?>Appearance</h2>
- <label>Search looks like</label>
- <select name="search_style">
- <option value="icon" <?= $settings['search_style'] === 'icon' ? 'selected' : '' ?>>A magnifying glass icon</option>
- <option value="field" <?= $settings['search_style'] === 'field' ? 'selected' : '' ?>>A search box you can type in</option>
- </select>
-
- <h2 class="dash-subtitle"><?= Icons::icon('layout', 'icon icon-sm') ?>Where to show it</h2>
- <label><input type="checkbox" name="search_show_in_nav" <?= $settings['search_show_in_nav'] === '1' ? 'checked' : '' ?>> In the top menu</label>
- <label><input type="checkbox" name="search_show_in_footer" <?= $settings['search_show_in_footer'] === '1' ? 'checked' : '' ?>> In the footer</label>
- <p style="font-size:12px;color:var(--muted);margin:-6px 0 16px;">Pick one, both, or neither.</p>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'reviews'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="reviews">
-
- <label><input type="checkbox" name="reviews_enabled" <?= $settings['reviews_enabled'] === '1' ? 'checked' : '' ?>> Let customers rate and review products (you approve them in Reviews)</label>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'custom-code'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="custom-code">
-
- <label>Custom CSS</label>
- <textarea name="custom_css" rows="8" class="be-code"><?= htmlspecialchars($settings['custom_css']) ?></textarea>
-
- <label>Custom JavaScript</label>
- <textarea name="custom_js" rows="8" class="be-code"><?= htmlspecialchars($settings['custom_js']) ?></textarea>
-
- <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Code in the page head</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 12px;">Added at the end of <head>. Good for verification codes, meta tags and trackers that need to load early.</p>
- <textarea name="custom_head_html" rows="8" class="be-code"><?= htmlspecialchars($settings['custom_head_html']) ?></textarea>
-
- <label style="margin-top:12px;">Show on</label>
- <label><input type="checkbox" name="custom_head_html_index" <?= $settings['custom_head_html_index'] === '1' ? 'checked' : '' ?>> Home page</label>
- <label><input type="checkbox" name="custom_head_html_product" <?= $settings['custom_head_html_product'] === '1' ? 'checked' : '' ?>> Products</label>
- <label><input type="checkbox" name="custom_head_html_category" <?= $settings['custom_head_html_category'] === '1' ? 'checked' : '' ?>> Categories</label>
- <label><input type="checkbox" name="custom_head_html_pages" <?= $settings['custom_head_html_pages'] === '1' ? 'checked' : '' ?>> Pages</label>
-
- <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Code at the end of the page</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 12px;">Added at the end of <body>. Good for chat widgets, pixels and other scripts that can load last.</p>
- <textarea name="custom_body_html" rows="8" class="be-code"><?= htmlspecialchars($settings['custom_body_html']) ?></textarea>
-
- <label style="margin-top:12px;">Show on</label>
- <label><input type="checkbox" name="custom_body_html_index" <?= $settings['custom_body_html_index'] === '1' ? 'checked' : '' ?>> Home page</label>
- <label><input type="checkbox" name="custom_body_html_product" <?= $settings['custom_body_html_product'] === '1' ? 'checked' : '' ?>> Products</label>
- <label><input type="checkbox" name="custom_body_html_category" <?= $settings['custom_body_html_category'] === '1' ? 'checked' : '' ?>> Categories</label>
- <label><input type="checkbox" name="custom_body_html_pages" <?= $settings['custom_body_html_pages'] === '1' ? 'checked' : '' ?>> Pages</label>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'email'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="email">
-
- <label>Send store emails (order confirmations and so on) with</label>
- <select name="mail_transport" id="mail_transport_select">
- <option value="smtp" <?= $settings['mail_transport'] === 'smtp' ? 'selected' : '' ?>>SMTP</option>
- <option value="resend" <?= $settings['mail_transport'] === 'resend' ? 'selected' : '' ?>>Resend</option>
- <option value="postmark" <?= $settings['mail_transport'] === 'postmark' ? 'selected' : '' ?>>Postmark</option>
- </select>
-
- <div id="mail-smtp-fields">
- <h2 class="dash-subtitle"><?= Icons::icon('mail', 'icon icon-sm') ?>SMTP</h2>
- <div class="settings-grid">
- <div>
- <label>Host</label>
- <input type="text" name="mail_smtp_host" value="<?= htmlspecialchars($settings['mail_smtp_host']) ?>" placeholder="smtp.example.com">
- </div>
- <div>
- <label>Port</label>
- <input type="number" name="mail_smtp_port" min="1" max="65535" value="<?= (int) $settings['mail_smtp_port'] ?>">
- </div>
- </div>
-
- <label>Username</label>
- <input type="text" name="mail_smtp_username" value="<?= htmlspecialchars($settings['mail_smtp_username']) ?>" autocomplete="off">
-
- <label>Password</label>
- <input type="password" name="mail_smtp_password" value="<?= htmlspecialchars($settings['mail_smtp_password']) ?>" autocomplete="new-password">
-
- <label>Encryption</label>
- <select name="mail_smtp_encryption">
- <option value="tls" <?= $settings['mail_smtp_encryption'] === 'tls' ? 'selected' : '' ?>>TLS (usually port 587)</option>
- <option value="ssl" <?= $settings['mail_smtp_encryption'] === 'ssl' ? 'selected' : '' ?>>SSL (usually port 465)</option>
- <option value="none" <?= $settings['mail_smtp_encryption'] === 'none' ? 'selected' : '' ?>>None</option>
- </select>
- </div>
-
- <div id="mail-resend-fields">
- <h2 class="dash-subtitle"><?= Icons::icon('mail', 'icon icon-sm') ?>Resend</h2>
- <label>API key</label>
- <input type="password" name="mail_resend_api_key" value="<?= htmlspecialchars($settings['mail_resend_api_key']) ?>" autocomplete="new-password" placeholder="re_...">
- </div>
-
- <div id="mail-postmark-fields">
- <h2 class="dash-subtitle"><?= Icons::icon('mail', 'icon icon-sm') ?>Postmark</h2>
- <label>Server token</label>
- <input type="password" name="mail_postmark_server_token" value="<?= htmlspecialchars($settings['mail_postmark_server_token']) ?>" autocomplete="new-password">
- </div>
-
- <script>
- (function() {
- var select = document.getElementById('mail_transport_select');
- var groups = {
- smtp: document.getElementById('mail-smtp-fields'),
- resend: document.getElementById('mail-resend-fields'),
- postmark: document.getElementById('mail-postmark-fields')
- };
-
- function apply() {
- Object.keys(groups).forEach(function(key) {
- groups[key].style.display = select.value === key ? '' : 'none';
- });
- }
-
- select.addEventListener('change', apply);
- apply();
- })();
- </script>
-
- <h2 class="dash-subtitle"><?= Icons::icon('mail', 'icon icon-sm') ?>Who the emails come from</h2>
- <label>Sender email</label>
- <input type="email" name="mail_from_email" value="<?= htmlspecialchars($settings['mail_from_email']) ?>" placeholder="orders@example.com">
-
- <label>Sender name</label>
- <input type="text" name="mail_from_name" value="<?= htmlspecialchars($settings['mail_from_name']) ?>" placeholder="<?= htmlspecialchars($settings['site_name']) ?>">
-
- <label>Replies go to (optional)</label>
- <input type="email" name="mail_reply_to" value="<?= htmlspecialchars($settings['mail_reply_to']) ?>">
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
-
- <div class="sidebar-box" style="max-width:520px;margin-top:20px;">
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('mail', 'icon icon-sm') ?>Send a test email</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:0 0 16px;">We'll send a short test message to you<?= !empty($currentUser['email']) ? ' (' . htmlspecialchars($currentUser['email']) . ')' : '' ?>. Save your settings first, the test uses what's saved.</p>
- <form method="post">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="send_test_email">
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('mail', 'icon icon-sm') ?>Send test email</button>
- </form>
- </div>
- <?php endif; ?>
-
- <?php if ($activeTab === 'footer'): ?>
- <form method="post" class="settings-section" id="footer-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_footer">
- <input type="hidden" name="footer_columns_json" id="footer_columns_json_input">
- <input type="hidden" name="footer_social_json" id="footer_social_json_input">
-
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('menu-list', 'icon icon-sm') ?>Footer links</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 16px;">Up to three columns of links at the bottom of your store, each with its own title.</p>
- <div id="footer-columns-root"></div>
- <button type="button" id="footer-add-column" class="dash-btn dash-btn-small" style="margin-top:4px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add column</button>
-
- <h2 class="dash-subtitle"><?= Icons::icon('globe', 'icon icon-sm') ?>Social media icons</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 16px;">Pick a ready icon or add your own SVG.</p>
- <div id="footer-social-root"></div>
- <div class="publish-actions" style="margin-top:4px;">
- <select id="footer-add-social-select" class="dash-input-inline" style="width:auto;">
- <?php foreach (SocialIcons::builtinLabels() as $key => $label): ?>
- <option value="<?= htmlspecialchars($key) ?>"><?= htmlspecialchars($label) ?></option>
- <?php endforeach; ?>
- </select>
- <button type="button" id="footer-add-social-builtin" class="dash-btn dash-btn-small"><?= Icons::icon('plus', 'icon icon-sm') ?>Add icon</button>
- <button type="button" id="footer-add-social-custom" class="dash-btn dash-btn-small"><?= Icons::icon('plus', 'icon icon-sm') ?>Add your own icon</button>
- </div>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
-
- <script>
- (function() {
- var initialColumns = <?= str_replace('<', '\u003C', FooterLinks::sanitizeColumns($settings['footer_columns'])) ?>;
- var initialSocial = <?= str_replace('<', '\u003C', FooterLinks::sanitizeSocialLinks($settings['footer_social_links'])) ?>;
-
- var columnsRoot = document.getElementById('footer-columns-root');
- var socialRoot = document.getElementById('footer-social-root');
-
- function el(tag, cls, text) {
- var e = document.createElement(tag);
- if (cls) e.className = cls;
- if (text !== undefined) e.textContent = text;
- return e;
- }
-
- function renderColumns() {
- columnsRoot.innerHTML = '';
- initialColumns.forEach(function(column, colIndex) {
- var box = el('div', 'sidebar-box');
- box.style.marginBottom = '12px';
-
- var headerRow = el('div', 'be-list-row');
- headerRow.style.marginBottom = '0';
-
- var titleLabel = el('label', null, 'Column name');
- titleLabel.style.marginTop = '0';
- titleLabel.style.marginBottom = '0';
- titleLabel.style.flex = '1';
- headerRow.appendChild(titleLabel);
-
- var moveBtnGroup = el('div', 'be-list-row');
- moveBtnGroup.style.marginBottom = '0';
- moveBtnGroup.style.gap = '8px';
- moveBtnGroup.style.flexShrink = '0';
-
- if (colIndex > 0) {
- var moveUpBtn = document.createElement('button');
- moveUpBtn.type = 'button';
- moveUpBtn.className = 'dash-btn-small';
- moveUpBtn.setAttribute('aria-label', 'Move column up');
- moveUpBtn.innerHTML = '<svg class="icon icon-sm" aria-hidden="true"><use href="#icon-arrow-up"></use></svg>';
- moveUpBtn.addEventListener('click', function() {
- var tmp = initialColumns[colIndex - 1];
- initialColumns[colIndex - 1] = initialColumns[colIndex];
- initialColumns[colIndex] = tmp;
- renderColumns();
- });
- moveBtnGroup.appendChild(moveUpBtn);
- }
-
- if (colIndex < initialColumns.length - 1) {
- var moveDownBtn = document.createElement('button');
- moveDownBtn.type = 'button';
- moveDownBtn.className = 'dash-btn-small';
- moveDownBtn.setAttribute('aria-label', 'Move column down');
- moveDownBtn.innerHTML = '<svg class="icon icon-sm" aria-hidden="true"><use href="#icon-arrow-down"></use></svg>';
- moveDownBtn.addEventListener('click', function() {
- var tmp = initialColumns[colIndex + 1];
- initialColumns[colIndex + 1] = initialColumns[colIndex];
- initialColumns[colIndex] = tmp;
- renderColumns();
- });
- moveBtnGroup.appendChild(moveDownBtn);
- }
-
- headerRow.appendChild(moveBtnGroup);
- box.appendChild(headerRow);
-
- var titleInput = document.createElement('input');
- titleInput.type = 'text';
- titleInput.value = column.title || '';
- titleInput.placeholder = 'TITLE';
- titleInput.addEventListener('input', function() {
- column.title = this.value;
- });
- box.appendChild(titleInput);
-
- var linksLabel = el('label', null, 'Links');
- box.appendChild(linksLabel);
-
- (column.links || []).forEach(function(link, linkIndex) {
- var row = el('div', 'be-list-row');
-
- var labelInput = document.createElement('input');
- labelInput.type = 'text';
- labelInput.placeholder = 'Label';
- labelInput.value = link.label || '';
- labelInput.style.flex = '1';
- labelInput.addEventListener('input', function() {
- link.label = this.value;
- });
-
- var urlInput = document.createElement('input');
- urlInput.type = 'text';
- urlInput.placeholder = 'https:// or /page';
- urlInput.value = link.url || '';
- urlInput.style.flex = '1';
- urlInput.addEventListener('input', function() {
- link.url = this.value;
- });
-
- var linkMoveBtnGroup = el('div', 'be-list-row');
- linkMoveBtnGroup.style.marginBottom = '0';
- linkMoveBtnGroup.style.gap = '8px';
- linkMoveBtnGroup.style.flexShrink = '0';
-
- if (linkIndex > 0) {
- var linkMoveUpBtn = document.createElement('button');
- linkMoveUpBtn.type = 'button';
- linkMoveUpBtn.className = 'dash-btn-small';
- linkMoveUpBtn.setAttribute('aria-label', 'Move link up');
- linkMoveUpBtn.innerHTML = '<svg class="icon icon-sm" aria-hidden="true"><use href="#icon-arrow-up"></use></svg>';
- linkMoveUpBtn.addEventListener('click', function() {
- var tmp = column.links[linkIndex - 1];
- column.links[linkIndex - 1] = column.links[linkIndex];
- column.links[linkIndex] = tmp;
- renderColumns();
- });
- linkMoveBtnGroup.appendChild(linkMoveUpBtn);
- }
-
- if (linkIndex < column.links.length - 1) {
- var linkMoveDownBtn = document.createElement('button');
- linkMoveDownBtn.type = 'button';
- linkMoveDownBtn.className = 'dash-btn-small';
- linkMoveDownBtn.setAttribute('aria-label', 'Move link down');
- linkMoveDownBtn.innerHTML = '<svg class="icon icon-sm" aria-hidden="true"><use href="#icon-arrow-down"></use></svg>';
- linkMoveDownBtn.addEventListener('click', function() {
- var tmp = column.links[linkIndex + 1];
- column.links[linkIndex + 1] = column.links[linkIndex];
- column.links[linkIndex] = tmp;
- renderColumns();
- });
- linkMoveBtnGroup.appendChild(linkMoveDownBtn);
- }
-
- var removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.className = 'dash-btn-small dash-btn-danger';
- removeBtn.textContent = '×';
- removeBtn.addEventListener('click', function() {
- column.links.splice(linkIndex, 1);
- renderColumns();
- });
-
- row.appendChild(labelInput);
- row.appendChild(urlInput);
- row.appendChild(linkMoveBtnGroup);
- row.appendChild(removeBtn);
- box.appendChild(row);
- });
-
- var addLinkBtn = document.createElement('button');
- addLinkBtn.type = 'button';
- addLinkBtn.className = 'dash-btn-small';
- addLinkBtn.textContent = 'Add link';
- addLinkBtn.addEventListener('click', function() {
- column.links = column.links || [];
- column.links.push({
- label: '',
- url: ''
- });
- renderColumns();
- });
- box.appendChild(addLinkBtn);
-
- var removeColumnBtn = document.createElement('button');
- removeColumnBtn.type = 'button';
- removeColumnBtn.className = 'dash-btn-small dash-btn-danger';
- removeColumnBtn.textContent = 'Remove column';
- removeColumnBtn.style.marginLeft = '8px';
- removeColumnBtn.addEventListener('click', function() {
- initialColumns.splice(colIndex, 1);
- renderColumns();
- });
- box.appendChild(removeColumnBtn);
-
- columnsRoot.appendChild(box);
- });
- }
-
- function renderSocial() {
- socialRoot.innerHTML = '';
- initialSocial.forEach(function(item, index) {
- var row = el('div', 'be-list-row');
-
- var iconLabel = el('span', 'be-block-type', item.key === 'custom' ? 'CUSTOM' : item.key.toUpperCase());
- iconLabel.style.width = '90px';
- iconLabel.style.flexShrink = '0';
-
- if (item.key === 'custom') {
- var nameInput = document.createElement('input');
- nameInput.type = 'text';
- nameInput.placeholder = 'Label';
- nameInput.value = item.label || '';
- nameInput.style.flex = '1';
- nameInput.addEventListener('input', function() {
- item.label = this.value;
- });
- row.appendChild(iconLabel);
- row.appendChild(nameInput);
- } else {
- row.appendChild(iconLabel);
- }
-
- var urlInput = document.createElement('input');
- urlInput.type = 'text';
- urlInput.placeholder = 'https://...';
- urlInput.value = item.url || '';
- urlInput.style.flex = '1';
- urlInput.addEventListener('input', function() {
- item.url = this.value;
- });
- row.appendChild(urlInput);
-
- var removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.className = 'dash-btn-small dash-btn-danger';
- removeBtn.textContent = '×';
- removeBtn.addEventListener('click', function() {
- initialSocial.splice(index, 1);
- renderSocial();
- });
- row.appendChild(removeBtn);
-
- socialRoot.appendChild(row);
-
- if (item.key === 'custom') {
- var svgLabel = el('label', null, 'SVG code');
- svgLabel.style.margin = '2px 0 4px';
- var svgTextarea = document.createElement('textarea');
- svgTextarea.rows = 2;
- svgTextarea.className = 'be-code';
- svgTextarea.style.minHeight = '50px';
- svgTextarea.value = item.svg || '';
- svgTextarea.placeholder = '<svg viewBox="0 0 24 24">...</svg>';
- svgTextarea.addEventListener('input', function() {
- item.svg = this.value;
- });
- socialRoot.appendChild(svgLabel);
- socialRoot.appendChild(svgTextarea);
- }
- });
- }
-
- document.getElementById('footer-add-column').addEventListener('click', function() {
- if (initialColumns.length >= 3) {
- alert('You can add up to three columns.');
- return;
- }
- initialColumns.push({
- title: '',
- links: [{
- label: '',
- url: ''
- }]
- });
- renderColumns();
- });
-
- document.getElementById('footer-add-social-builtin').addEventListener('click', function() {
- var select = document.getElementById('footer-add-social-select');
- initialSocial.push({
- key: select.value,
- label: select.options[select.selectedIndex].text,
- url: ''
- });
- renderSocial();
- });
-
- document.getElementById('footer-add-social-custom').addEventListener('click', function() {
- initialSocial.push({
- key: 'custom',
- label: '',
- url: '',
- svg: ''
- });
- renderSocial();
- });
-
- document.getElementById('footer-form').addEventListener('submit', function() {
- document.getElementById('footer_columns_json_input').value = JSON.stringify(initialColumns);
- document.getElementById('footer_social_json_input').value = JSON.stringify(initialSocial);
- });
-
- renderColumns();
- renderSocial();
- })();
- </script>
- <?php endif; ?>
-
- <?php if ($activeTab === 'apps'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="apps">
-
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('smartphone', 'icon icon-sm') ?>Install as an app</h2>
- <label><input type="checkbox" name="pwa_enabled" <?= $settings['pwa_enabled'] === '1' ? 'checked' : '' ?>> Let visitors install your store on their phone or computer like an app</label>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 16px;">Visitors get the option to add your store to their home screen.</p>
-
- <label>App name (leave empty to use your store name)</label>
- <input type="text" name="pwa_name" value="<?= htmlspecialchars($settings['pwa_name']) ?>" placeholder="<?= htmlspecialchars($settings['site_name']) ?>">
-
- <label>Short name under the icon (leave empty to use the app name)</label>
- <input type="text" name="pwa_short_name" value="<?= htmlspecialchars($settings['pwa_short_name']) ?>" maxlength="12" placeholder="<?= htmlspecialchars($settings['site_name']) ?>">
-
- <label>Description (leave empty to use your store description)</label>
- <textarea name="pwa_description" rows="2" placeholder="<?= htmlspecialchars($settings['site_description']) ?>"><?= htmlspecialchars($settings['pwa_description']) ?></textarea>
-
- <label>Icon (leave empty to use your logo)</label>
- <input type="text" name="pwa_icon_url" data-image-upload value="<?= htmlspecialchars($settings['pwa_icon_url']) ?>" placeholder="media/img/pwa-icon.png">
- <p style="font-size:12px;color:var(--muted);margin-top:6px;">Use a square image, 512×512 or bigger.</p>
-
- <div class="settings-grid">
- <div>
- <label>Theme color</label>
- <input type="color" name="pwa_theme_color" value="<?= htmlspecialchars($settings['pwa_theme_color']) ?>">
- </div>
- <div>
- <label>Background color</label>
- <input type="color" name="pwa_background_color" value="<?= htmlspecialchars($settings['pwa_background_color']) ?>">
- </div>
- </div>
-
- <h2 class="dash-subtitle"><?= Icons::icon('link', 'icon icon-sm') ?>Mobile app links</h2>
- <label><input type="checkbox" name="app_banner_enabled" <?= $settings['app_banner_enabled'] === '1' ? 'checked' : '' ?>> Show phone visitors a banner with a link to your app</label>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 16px;">Android users see the Google Play link and iPhone users see the App Store link. Nobody sees it on a computer, or if the link for their phone is empty.</p>
-
- <label>Google Play link (Android)</label>
- <input type="text" name="app_android_url" value="<?= htmlspecialchars($settings['app_android_url']) ?>" placeholder="https://play.google.com/store/apps/details?id=...">
-
- <label>App Store link (iPhone)</label>
- <input type="text" name="app_ios_url" value="<?= htmlspecialchars($settings['app_ios_url']) ?>" placeholder="https://apps.apple.com/app/id...">
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php endif; ?>
-
- <?php if ($activeTab === 'ai'): ?>
- <?php
- $aiSettings = Ai::settings();
- $aiRoleLabels = [];
- foreach (Ai::roleKeys() as $aiRoleKey) {
- $aiRoleLabels[$aiRoleKey] = Auth::roleLabel($aiRoleKey);
- }
- ?>
- <form method="post" class="settings-section" id="ai-settings-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_settings">
- <input type="hidden" name="settings_tab" value="ai">
-
- <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">A chat assistant that helps write products and pages. It's off until you turn it on. When someone uses it, the text they're editing goes to the AI company you pick below. It never saves anything on its own, you see every suggestion first and can undo it.</p>
-
- <label><input type="checkbox" name="ai_enabled" <?= $aiSettings['enabled'] ? 'checked' : '' ?>> Turn on the AI assistant</label>
-
- <label>AI company</label>
- <select name="ai_provider" id="ai-provider-select">
- <?php foreach (Ai::PROVIDERS as $aiProviderKey => $aiProviderLabel): ?>
- <option value="<?= htmlspecialchars($aiProviderKey) ?>" <?= $aiSettings['provider'] === $aiProviderKey ? 'selected' : '' ?>><?= htmlspecialchars($aiProviderLabel) ?></option>
- <?php endforeach; ?>
- </select>
-
- <?php foreach (Ai::PROVIDERS as $aiProviderKey => $aiProviderLabel): ?>
- <?php
- $aiKeyState = Ai::keyState($aiProviderKey);
- $aiCurrentModel = $aiSettings['models'][$aiProviderKey];
- $aiIsPreset = isset(Ai::MODELS[$aiProviderKey][$aiCurrentModel]);
- ?>
- <div class="sidebar-box ai-provider-panel" data-provider="<?= htmlspecialchars($aiProviderKey) ?>" style="margin-top:16px;">
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('sparkles', 'icon icon-sm') ?><?= htmlspecialchars($aiProviderLabel) ?></h2>
-
- <label>Model</label>
- <select name="ai_model_<?= htmlspecialchars($aiProviderKey) ?>" class="ai-model-select">
- <?php foreach (Ai::MODELS[$aiProviderKey] as $aiModelId => $aiModelLabel): ?>
- <option value="<?= htmlspecialchars($aiModelId) ?>" <?= $aiCurrentModel === $aiModelId ? 'selected' : '' ?>><?= htmlspecialchars($aiModelLabel) ?> (<?= htmlspecialchars($aiModelId) ?>)</option>
- <?php endforeach; ?>
- <option value="custom" <?= $aiIsPreset ? '' : 'selected' ?>>Other model…</option>
- </select>
- <input type="text" name="ai_model_<?= htmlspecialchars($aiProviderKey) ?>_custom" class="ai-model-custom" value="<?= $aiIsPreset ? '' : htmlspecialchars($aiCurrentModel) ?>" placeholder="model-id" maxlength="80" style="margin-top:8px;<?= $aiIsPreset ? 'display:none;' : '' ?>">
-
- <label>API key</label>
- <input type="password" name="ai_key_<?= htmlspecialchars($aiProviderKey) ?>" autocomplete="new-password" spellcheck="false" placeholder="<?= $aiKeyState['hint'] !== null ? 'Saved key ending in ' . htmlspecialchars($aiKeyState['hint']) . ', leave empty to keep it' : 'Paste your API key' ?>">
- <?php if ($aiKeyState['environment']): ?>
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">There's also a key in your server's config file (AI_KEY_<?= strtoupper(htmlspecialchars($aiProviderKey)) ?>), and that one is used first.</p>
- <?php endif; ?>
- <?php if ($aiKeyState['hint'] !== null): ?>
- <label><input type="checkbox" name="ai_key_remove_<?= htmlspecialchars($aiProviderKey) ?>"> Remove the saved key</label>
- <?php endif; ?>
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">We keep the key encrypted and never show it again.</p>
- </div>
- <?php endforeach; ?>
-
- <h2 class="dash-subtitle"><?= Icons::icon('toggle', 'icon icon-sm') ?>What it's allowed to do</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 12px;">The assistant only does what's ticked here, no matter what someone asks.</p>
- <?php foreach (Ai::FEATURES as $aiFeatureKey => [$aiFeatureLabel, $aiFeatureHelp]): ?>
- <label><input type="checkbox" name="ai_features[]" value="<?= htmlspecialchars($aiFeatureKey) ?>" <?= in_array($aiFeatureKey, $aiSettings['features'], true) ? 'checked' : '' ?>> <span><strong><?= htmlspecialchars($aiFeatureLabel) ?></strong>, <?= htmlspecialchars($aiFeatureHelp) ?></span></label>
- <?php endforeach; ?>
-
- <h2 class="dash-subtitle"><?= Icons::icon('users', 'icon icon-sm') ?>Who can use it</h2>
- <p style="font-size:12.5px;color:var(--muted);margin:-6px 0 12px;">The Super Admin can always use it. Anyone can hide it for themselves in Appearance.</p>
- <?php foreach ($aiRoleLabels as $aiRoleKey => $aiRoleLabel): ?>
- <label><input type="checkbox" name="ai_roles[]" value="<?= htmlspecialchars($aiRoleKey) ?>" <?= in_array($aiRoleKey, $aiSettings['roles'], true) ? 'checked' : '' ?>> <?= htmlspecialchars($aiRoleLabel) ?></label>
- <?php endforeach; ?>
-
- <h2 class="dash-subtitle"><?= Icons::icon('shield', 'icon icon-sm') ?>Limits</h2>
- <div class="settings-grid">
- <div>
- <label>Messages per person per hour</label>
- <input type="number" name="ai_rate_limit" min="1" max="1000" value="<?= (int) $aiSettings['rate_limit'] ?>">
- </div>
- <div>
- <label>Longest answer (in tokens)</label>
- <input type="number" name="ai_max_tokens" min="256" max="32000" value="<?= (int) $aiSettings['max_tokens'] ?>">
- </div>
- </div>
-
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
-
- <script>
- (function() {
- var providerSelect = document.getElementById('ai-provider-select');
- var panels = document.querySelectorAll('.ai-provider-panel');
-
- function showProvider() {
- panels.forEach(function(panel) {
- panel.style.display = panel.dataset.provider === providerSelect.value ? '' : 'none';
- });
- }
-
- providerSelect.addEventListener('change', showProvider);
- showProvider();
-
- document.querySelectorAll('.ai-model-select').forEach(function(select) {
- var custom = select.parentNode.querySelector('.ai-model-custom');
-
- function toggleCustom() {
- custom.style.display = select.value === 'custom' ? '' : 'none';
- }
-
- select.addEventListener('change', toggleCustom);
- toggleCustom();
- });
- })();
- </script>
- <?php endif; ?>
-
- <?php if ($activeTab === 'language'): ?>
- <form method="post" class="settings-section">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_language">
- <p style="font-size:12.5px;color:var(--muted);margin:0 0 16px;">Change any text your customers see in the store. The small grey name above each field is only there so you can find it.</p>
- <?php foreach ($languageStrings as $key => $value): ?>
- <div class="lang-row">
- <div class="lang-row-key"><?= htmlspecialchars($key) ?></div>
- <input type="text" name="lang_<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value) ?>">
- </div>
- <?php endforeach; ?>
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save texts</button>
- </form>
- <?php endif; ?>
-
- <?php require __DIR__ . '/includes/dash-footer.php'; ?>