WebOrbiton

Code

StocketBase

Self-hosted digital commerce, without commission fees or SaaS lock-in.

Version history

Version 1.0.0.1 2026-09-26

Browse files
Files changed65
Lines added+1039
Lines removed-593
activity.php Modified +8 -8
⋮ 20 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'clear_activity_log') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $days = max(0, (int) ($_POST['older_than_days'] ?? 0));
⋮ 4 unchanged lines ⋮
          } else {
              $db->exec('DELETE FROM activity_log');
          }
-         $flashMessage = 'Activity log cleared.';
+         $flashMessage = 'Done, those entries are gone.';
      }
  }
  
⋮ 23 unchanged lines ⋮
  <h1 class="dash-title"><?= Icons::icon('stats', 'icon icon-lg') ?>Activity log</h1>
  
  <?php if (!$logEnabled): ?>
- <div class="dash-flash dash-flash-error"><?= Icons::icon('x', 'icon icon-sm') ?>Activity log is turned off, so new actions are not being recorded. Enable it in Settings → General.</div>
+ <div class="dash-flash dash-flash-error"><?= Icons::icon('x', 'icon icon-sm') ?>The activity log is off, so nothing new is being recorded. You can turn it on in Settings, General.</div>
  <?php endif; ?>
  
  <table class="dash-table">
- <thead><tr><th>When</th><th>User</th><th>Action</th><th>Item</th><th>Details</th><th>IP</th></tr></thead>
+ <thead><tr><th>When</th><th>Who</th><th>What they did</th><th>On</th><th>Details</th><th>IP address</th></tr></thead>
  <tbody>
  <?php foreach ($entries as $entry): ?>
  <tr>
⋮ 6 unchanged lines ⋮
  </tr>
  <?php endforeach; ?>
  <?php if (empty($entries)): ?>
- <tr><td colspan="6">No activity recorded yet.</td></tr>
+ <tr><td colspan="6">Nothing recorded yet.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 6 unchanged lines ⋮
  </p>
  <?php endif; ?>
  
- <form method="post" style="margin-top:24px;" onsubmit="return confirm('Delete these log entries?');">
+ <form method="post" style="margin-top:24px;" onsubmit="return confirm('Delete these entries? You can\'t get them back.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="clear_activity_log">
- <label>Clear entries</label>
+ <label>Clean up the log</label>
  <select name="older_than_days">
  <option value="30">Older than 30 days</option>
  <option value="90">Older than 90 days</option>
  <option value="365">Older than 1 year</option>
- <option value="0">All entries</option>
+ <option value="0">Everything</option>
  </select>
  <button type="submit" class="dash-btn dash-btn-danger" style="margin-top:12px;"><?= Icons::icon('trash', 'icon icon-sm') ?>Clear</button>
  </form>
⋮ 3 unchanged lines ⋮
admin.php Modified +169 -55
⋮ 40 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = $_POST['action'] ?? '';
  
          if ($action === 'suspend_customer') {
              if (!$isSuperAdmin) {
-                 $flashMessage = 'Only the Super Admin can suspend customer accounts.';
+                 $flashMessage = 'Only the Super Admin can block customers.';
                  $flashType = 'error';
              } else {
                  $customerId = (int) ($_POST['customer_id'] ?? 0);
                  $update = $usersDb->prepare("UPDATE user_accounts SET status = 'suspended' WHERE id = :id");
                  $update->execute(['id' => $customerId]);
-                 $flashMessage = 'Customer account suspended.';
+                 $flashMessage = 'Customer blocked. They can no longer log in.';
              }
              $activeTab = 'customers';
          }
  
          if ($action === 'reactivate_customer') {
              if (!$isSuperAdmin) {
-                 $flashMessage = 'Only the Super Admin can reactivate customer accounts.';
+                 $flashMessage = 'Only the Super Admin can unblock customers.';
                  $flashType = 'error';
              } else {
                  $customerId = (int) ($_POST['customer_id'] ?? 0);
                  $update = $usersDb->prepare("UPDATE user_accounts SET status = 'active' WHERE id = :id");
                  $update->execute(['id' => $customerId]);
-                 $flashMessage = 'Customer account reactivated.';
+                 $flashMessage = 'Customer unblocked.';
              }
              $activeTab = 'customers';
          }
⋮ 6 unchanged lines ⋮
              $role = (string) ($_POST['role'] ?? '');
  
              if ($username === '' || $email === '' || $password === '' || !in_array($role, $assignableRoles, true)) {
-                 $flashMessage = 'All fields are required and role must be valid for your permission level.';
+                 $flashMessage = 'Please fill in every field and pick a role you are allowed to give.';
                  $flashType = 'error';
              } elseif ($role === Auth::ROLE_SUPER_ADMIN) {
-                 $flashMessage = 'Only one Super Admin account may exist; it cannot be created here.';
+                 $flashMessage = 'There can only be one Super Admin.';
                  $flashType = 'error';
              } elseif (strlen($password) < 10) {
-                 $flashMessage = 'Password must be at least 10 characters.';
+                 $flashMessage = 'The password needs at least 10 characters.';
                  $flashType = 'error';
              } else {
                  $newAvatar = null;
⋮ 17 unchanged lines ⋮
                          'status' => 'active',
                          'avatar' => $newAvatar,
                      ]);
-                     $flashMessage = 'Account created.';
+                     $flashMessage = 'Account created. They can log in now.';
                  } catch (InvalidArgumentException $exception) {
                      $flashMessage = $exception->getMessage();
                      $flashType = 'error';
                  } catch (Throwable $exception) {
                      Avatar::delete($newAvatar);
-                     $flashMessage = 'Could not create account (username or email may already exist).';
+                     $flashMessage = 'That username or email is already taken.';
                      $flashType = 'error';
                  }
              }
⋮ 20 unchanged lines ⋮
                  $flashMessage = 'Account not found.';
                  $flashType = 'error';
              } elseif (!$isSuperAdmin && !$isSelfEdit && !in_array($targetAccount['role'], $assignableRoles, true)) {
-                 $flashMessage = 'You can only manage accounts with a lower role than yours.';
+                 $flashMessage = 'You can only change people with a lower role than yours.';
                  $flashType = 'error';
              } elseif ($targetAccount['role'] === Auth::ROLE_SUPER_ADMIN && (int) $targetAccount['id'] !== (int) $currentUser['id']) {
-                 $flashMessage = 'The Super Admin account can only be edited by itself.';
+                 $flashMessage = 'Only the Super Admin can change their own account.';
                  $flashType = 'error';
              } elseif ($role === Auth::ROLE_SUPER_ADMIN && $targetAccount['role'] !== Auth::ROLE_SUPER_ADMIN) {
-                 $flashMessage = 'Super Admin role cannot be assigned to another account.';
+                 $flashMessage = 'You can\'t make someone else Super Admin.';
                  $flashType = 'error';
              } elseif (!$isSelfEdit && !in_array($role, array_merge($assignableRoles, [Auth::ROLE_SUPER_ADMIN]), true)) {
-                 $flashMessage = 'Invalid role for your permission level.';
+                 $flashMessage = 'You can\'t give that role.';
                  $flashType = 'error';
              } else {
                  $fields = ['display_name = :display_name', 'role = :role', 'status = :status'];
⋮ 9 unchanged lines ⋮
  
                  if ($newPassword !== '') {
                      if (strlen($newPassword) < 10) {
-                         $saveError = 'New password must be at least 10 characters.';
+                         $saveError = 'The new password needs at least 10 characters.';
                      } else {
                          $fields[] = 'password_hash = :hash';
                          $params['hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
⋮ 36 unchanged lines ⋮
                          Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
                      }
  
-                     $flashMessage = 'Account updated.';
+                     $flashMessage = 'Changes saved.';
                  }
              }
          }
⋮ 9 unchanged lines ⋮
                  $flashMessage = 'Account not found.';
                  $flashType = 'error';
              } elseif ($targetAccount['role'] === Auth::ROLE_SUPER_ADMIN) {
-                 $flashMessage = 'The Super Admin account cannot be deleted.';
+                 $flashMessage = 'The Super Admin can\'t be deleted.';
                  $flashType = 'error';
              } elseif ((int) $targetAccount['id'] === (int) $currentUser['id']) {
-                 $flashMessage = 'You cannot delete your own account.';
+                 $flashMessage = 'You can\'t delete your own account.';
                  $flashType = 'error';
              } elseif (!$isSuperAdmin && !in_array($targetAccount['role'], $assignableRoles, true)) {
-                 $flashMessage = 'You can only manage accounts with a lower role than yours.';
+                 $flashMessage = 'You can only change people with a lower role than yours.';
                  $flashType = 'error';
              } else {
-                 $delete = $db->prepare('DELETE FROM team_accounts WHERE id = :id');
-                 $delete->execute(['id' => $targetId]);
-                 Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
-                 $flashMessage = 'Account deleted.';
+                 $transferTo = (int) ($_POST['transfer_to'] ?? 0);
+                 $receiver = null;
+                 if ($transferTo > 0 && $transferTo !== $targetId) {
+                     $receiverStatement = $db->prepare('SELECT id, display_name FROM team_accounts WHERE id = :id LIMIT 1');
+                     $receiverStatement->execute(['id' => $transferTo]);
+                     $receiver = $receiverStatement->fetch() ?: null;
+                 }
+ 
+                 if ($receiver === null) {
+                     $flashMessage = 'Pick who should take over this person\'s products, pages, ads and discount codes.';
+                     $flashType = 'error';
+                 } else {
+                     try {
+                         $db->beginTransaction();
+                         $transferSql = [
+                             'UPDATE products SET created_by = :to WHERE created_by = :from',
+                             'UPDATE pages SET author_id = :to WHERE author_id = :from AND is_home = 0',
+                             'UPDATE ads SET created_by = :to WHERE created_by = :from',
+                             'UPDATE discounts SET created_by = :to WHERE created_by = :from',
+                         ];
+                         foreach ($transferSql as $sql) {
+                             $db->prepare($sql)->execute(['to' => (int) $receiver['id'], 'from' => $targetId]);
+                         }
+                         $db->prepare('UPDATE pages SET author_id = NULL WHERE author_id = :from AND is_home = 1')->execute(['from' => $targetId]);
+                         $delete = $db->prepare('DELETE FROM team_accounts WHERE id = :id');
+                         $delete->execute(['id' => $targetId]);
+                         $db->commit();
+                         Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
+                         $flashMessage = 'Account deleted. Everything they made now belongs to ' . $receiver['display_name'] . '.';
+                     } catch (Throwable $exception) {
+                         if ($db->inTransaction()) {
+                             $db->rollBack();
+                         }
+                         error_log('StocketBase: account delete failed: ' . $exception->getMessage());
+                         $flashMessage = 'Something went wrong, so nothing was deleted. Please try again.';
+                         $flashType = 'error';
+                     }
+                 }
              }
          }
      }
⋮ 1 unchanged line ⋮
  
  $accounts = $db->query('SELECT * FROM team_accounts ORDER BY FIELD(role, \'super_admin\',\'store_owner\',\'store_manager\',\'catalog_editor\',\'product_editor\',\'catalog_assistant\'), display_name ASC')->fetchAll();
  
+ $ownedCounts = [];
+ foreach ([
+     'products' => 'SELECT created_by AS owner, COUNT(*) AS c FROM products WHERE created_by IS NOT NULL GROUP BY created_by',
+     'pages' => 'SELECT author_id AS owner, COUNT(*) AS c FROM pages WHERE author_id IS NOT NULL AND is_home = 0 GROUP BY author_id',
+     'ads' => 'SELECT created_by AS owner, COUNT(*) AS c FROM ads WHERE created_by IS NOT NULL GROUP BY created_by',
+     'discounts' => 'SELECT created_by AS owner, COUNT(*) AS c FROM discounts WHERE created_by IS NOT NULL GROUP BY created_by',
+ ] as $ownedKey => $ownedSql) {
+     try {
+         foreach ($db->query($ownedSql)->fetchAll() as $ownedRow) {
+             $ownedCounts[(int) $ownedRow['owner']][$ownedKey] = (int) $ownedRow['c'];
+         }
+     } catch (PDOException $exception) {
+     }
+ }
+ 
  $customers = [];
  if ($isSuperAdmin) {
      $customers = $usersDb->query('SELECT * FROM user_accounts ORDER BY created_at DESC')->fetchAll();
  }
  
  $dashActivePage = 'admin';
- $dashPageTitle = 'Team accounts';
+ $dashPageTitle = 'Team';
  
  require __DIR__ . '/includes/dash-header.php';
  
⋮ 7 unchanged lines ⋮
  
  <?php if ($isSuperAdmin): ?>
      <div class="settings-tabs">
-         <a href="admin.php?tab=team" class="<?= $activeTab === 'team' ? 'active' : '' ?>"><?= Icons::icon("team", "icon icon-sm") ?>Team accounts</a>
-         <a href="admin.php?tab=customers" class="<?= $activeTab === 'customers' ? 'active' : '' ?>"><?= Icons::icon("book", "icon icon-sm") ?>Customer accounts</a>
+         <a href="admin.php?tab=team" class="<?= $activeTab === 'team' ? 'active' : '' ?>"><?= Icons::icon("team", "icon icon-sm") ?>Your team</a>
+         <a href="admin.php?tab=customers" class="<?= $activeTab === 'customers' ? 'active' : '' ?>"><?= Icons::icon("book", "icon icon-sm") ?>Customers</a>
      </div>
  <?php endif; ?>
  
⋮ 6 unchanged lines ⋮
                  <th><?= Icons::icon('user', 'icon icon-sm') ?>Email</th>
                  <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
                  <th><?= Icons::icon('stats', 'icon icon-sm') ?>Registered</th>
-                 <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last login</th>
+                 <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last seen</th>
                  <th></th>
              </tr>
          </thead>
          <tbody>
              <?php foreach ($customers as $customer): ?>
                  <tr>
-                     <td><?= htmlspecialchars((string) ($customer['display_name'] ?? '—')) ?></td>
+                     <td><?= htmlspecialchars((string) ($customer['display_name'] ?? 'No name')) ?></td>
                      <td><?= htmlspecialchars($customer['email']) ?></td>
-                     <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars($customer['status']) ?></span></td>
+                     <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars(StatusLabel::get((string) $customer['status'])) ?></span></td>
                      <td><?= htmlspecialchars($customer['created_at']) ?></td>
-                     <td><?= htmlspecialchars((string) ($customer['last_login_at'] ?? '—')) ?></td>
+                     <td><?= htmlspecialchars((string) ($customer['last_login_at'] ?? 'Never')) ?></td>
                      <td class="dash-table-actions">
                          <?php if ($customer['status'] === 'active'): ?>
-                             <form method="post" style="display:inline;" onsubmit="return confirm('Suspend this customer account? They will be logged out and unable to sign in.');">
+                             <form method="post" style="display:inline;" onsubmit="return confirm('Block this customer? They will be logged out and won\'t be able to log in.');">
                                  <?= Csrf::field() ?>
                                  <input type="hidden" name="action" value="suspend_customer">
                                  <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
-                                 <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('lock', 'icon icon-sm') ?>Suspend</button>
+                                 <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('lock', 'icon icon-sm') ?>Block</button>
                              </form>
                          <?php else: ?>
                              <form method="post" style="display:inline;">
                                  <?= Csrf::field() ?>
                                  <input type="hidden" name="action" value="reactivate_customer">
                                  <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
-                                 <button type="submit" class="dash-btn-small dash-btn-success"><?= Icons::icon('check', 'icon icon-sm') ?>Reactivate</button>
+                                 <button type="submit" class="dash-btn-small dash-btn-success"><?= Icons::icon('check', 'icon icon-sm') ?>Unblock</button>
                              </form>
                          <?php endif; ?>
                      </td>
⋮ 1 unchanged line ⋮
              <?php endforeach; ?>
              <?php if (empty($customers)): ?>
                  <tr>
-                     <td colspan="6">No customer accounts yet.</td>
+                     <td colspan="6">No customers yet.</td>
                  </tr>
              <?php endif; ?>
          </tbody>
⋮ 10 unchanged lines ⋮
                  <th><?= Icons::icon('team', 'icon icon-sm') ?>Role</th>
                  <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
                  <th><?= Icons::icon('lock', 'icon icon-sm') ?>2FA</th>
-                 <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last login</th>
+                 <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last seen</th>
                  <th></th>
              </tr>
          </thead>
⋮ 4 unchanged lines ⋮
                      <td><?= htmlspecialchars($account['username']) ?></td>
                      <td><?= htmlspecialchars((string) $account['email']) ?></td>
                      <td><?= htmlspecialchars(Auth::roleLabel($account['role'])) ?></td>
-                     <td><?= htmlspecialchars($account['status']) ?></td>
-                     <td><?= TwoFactor::isEnabled($account) ? 'On' : '—' ?></td>
-                     <td><?= htmlspecialchars((string) ($account['last_login_at'] ?? '—')) ?></td>
+                     <td><?= htmlspecialchars(StatusLabel::get((string) $account['status'])) ?></td>
+                     <td><?= TwoFactor::isEnabled($account) ? 'On' : 'Off' ?></td>
+                     <td><?= htmlspecialchars((string) ($account['last_login_at'] ?? 'Never')) ?></td>
                      <td class="dash-table-actions">
                          <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN || (int) $account['id'] === (int) $currentUser['id']): ?>
                              <button type="button" class="dash-btn-small js-edit-account"
⋮ 5 unchanged lines ⋮
                                  data-status="<?= htmlspecialchars($account['status'], ENT_QUOTES) ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</button>
                          <?php endif; ?>
                          <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN && (int) $account['id'] !== (int) $currentUser['id']): ?>
-                             <form method="post" style="display:inline;" onsubmit="return confirm('Delete this account?');">
-                                 <?= Csrf::field() ?>
-                                 <input type="hidden" name="action" value="delete_account">
-                                 <input type="hidden" name="account_id" value="<?= (int) $account['id'] ?>">
-                                 <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
-                             </form>
+                             <?php $owned = $ownedCounts[(int) $account['id']] ?? []; ?>
+                             <button type="button" class="dash-btn-small dash-btn-danger js-delete-account"
+                                 data-id="<?= (int) $account['id'] ?>"
+                                 data-name="<?= htmlspecialchars($account['display_name'], ENT_QUOTES) ?>"
+                                 data-products="<?= (int) ($owned['products'] ?? 0) ?>"
+                                 data-pages="<?= (int) ($owned['pages'] ?? 0) ?>"
+                                 data-ads="<?= (int) ($owned['ads'] ?? 0) ?>"
+                                 data-discounts="<?= (int) ($owned['discounts'] ?? 0) ?>"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
                          <?php endif; ?>
                      </td>
                  </tr>
⋮ 1 unchanged line ⋮
          </tbody>
      </table>
  
-     <h2 class="dash-subtitle"><?= Icons::icon('plus', 'icon icon-sm') ?>Create account</h2>
+     <div class="sidebar-box" id="delete-account-box" style="display:none;margin-top:20px;">
+         <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete account: <span id="delete_account_name"></span></h2>
+         <p id="delete_account_summary" style="color:var(--muted);margin:0 0 14px;"></p>
+         <form method="post" id="delete-account-form">
+             <?= Csrf::field() ?>
+             <input type="hidden" name="action" value="delete_account">
+             <input type="hidden" name="account_id" id="delete_account_id" value="">
+ 
+             <label><?= Icons::icon('users', 'icon icon-sm') ?>Who takes over their products, pages, ads and discount codes?</label>
+             <select name="transfer_to" id="delete_transfer_to" required>
+                 <?php foreach ($accounts as $receiverAccount): ?>
+                     <option value="<?= (int) $receiverAccount['id'] ?>" <?= (int) $receiverAccount['id'] === (int) $currentUser['id'] ? 'selected' : '' ?>><?= htmlspecialchars($receiverAccount['display_name']) ?> (<?= htmlspecialchars(Auth::roleLabel($receiverAccount['role'])) ?>)</option>
+                 <?php endforeach; ?>
+             </select>
+ 
+             <div class="publish-actions">
+                 <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Hand over and delete</button>
+                 <button type="button" class="dash-btn" onclick="document.getElementById('delete-account-box').style.display='none';"><?= Icons::icon('x', 'icon icon-sm') ?>Cancel</button>
+             </div>
+         </form>
+     </div>
+ 
+     <h2 class="dash-subtitle"><?= Icons::icon('plus', 'icon icon-sm') ?>Add a team member</h2>
      <form method="post" enctype="multipart/form-data">
          <?= Csrf::field() ?>
          <input type="hidden" name="action" value="create_account">
⋮ 4 unchanged lines ⋮
          <label><?= Icons::icon('hash', 'icon icon-sm') ?>Email</label>
          <input type="text" name="email" required>
  
-         <label><?= Icons::icon('heading', 'icon icon-sm') ?>Display name</label>
+         <label><?= Icons::icon('heading', 'icon icon-sm') ?>Name shown in the dashboard</label>
          <input type="text" name="display_name">
  
          <label><?= Icons::icon('lock', 'icon icon-sm') ?>Password</label>
          <input type="password" name="password" minlength="10" required>
  
-         <label><?= Icons::icon('user', 'icon icon-sm') ?>Profile photo (optional, JPG / PNG / WebP, max 2MB)</label>
+         <label><?= Icons::icon('user', 'icon icon-sm') ?>Photo (optional, JPG, PNG or WebP up to 2 MB)</label>
          <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
  
          <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
⋮ 6 unchanged lines ⋮
              <?php endforeach; ?>
          </select>
  
-         <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Create account</button>
+         <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add to team</button>
      </form>
  
-     <h2 class="dash-subtitle" id="edit-account-title" style="display:none;"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit account</h2>
+     <h2 class="dash-subtitle" id="edit-account-title" style="display:none;"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit team member</h2>
      <form method="post" id="edit-account-form" style="display:none;" enctype="multipart/form-data">
          <?= Csrf::field() ?>
          <input type="hidden" name="action" value="update_account">
          <input type="hidden" name="account_id" id="edit_account_id">
  
-         <label><?= Icons::icon('user', 'icon icon-sm') ?>Profile photo (JPG / PNG / WebP, max 2MB)</label>
+         <label><?= Icons::icon('user', 'icon icon-sm') ?>Photo (JPG, PNG or WebP up to 2 MB)</label>
          <div class="avatar-edit-preview">
              <div class="dash-user-avatar" id="edit_avatar_preview"></div>
              <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
          </div>
-         <label><input type="checkbox" name="remove_avatar" id="edit_remove_avatar"> Remove current photo (the initial letter is shown instead)</label>
+         <label><input type="checkbox" name="remove_avatar" id="edit_remove_avatar"> Remove the photo (we'll show their first letter instead)</label>
  
-         <label><?= Icons::icon('heading', 'icon icon-sm') ?>Display name</label>
+         <label><?= Icons::icon('heading', 'icon icon-sm') ?>Name shown in the dashboard</label>
          <input type="text" name="display_name" id="edit_display_name">
  
          <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
⋮ 7 unchanged lines ⋮
          <label><?= Icons::icon('flag', 'icon icon-sm') ?>Status</label>
          <select name="status" id="edit_status">
              <option value="active">Active</option>
-             <option value="suspended">Suspended</option>
+             <option value="suspended">Blocked</option>
          </select>
  
-         <label><?= Icons::icon('lock', 'icon icon-sm') ?>New password (leave blank to keep current)</label>
+         <label><?= Icons::icon('lock', 'icon icon-sm') ?>New password (leave empty to keep the old one)</label>
          <input type="password" name="new_password" minlength="10">
  
-         <label><input type="checkbox" name="reset_2fa" id="edit_reset_2fa"> Reset two-factor authentication (use when the user lost their phone and recovery codes)</label>
+         <label><input type="checkbox" name="reset_2fa" id="edit_reset_2fa"> Turn off their two-step login (if they lost their phone and backup codes)</label>
  
          <div class="publish-actions">
              <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save changes</button>
⋮ 2 unchanged lines ⋮
      </form>
  
      <script>
+         document.querySelectorAll('.js-delete-account').forEach(function(button) {
+             button.addEventListener('click', function() {
+                 var box = document.getElementById('delete-account-box');
+                 var select = document.getElementById('delete_transfer_to');
+                 var targetId = this.dataset.id;
+                 document.getElementById('delete_account_id').value = targetId;
+                 document.getElementById('delete_account_name').textContent = this.dataset.name;
+                 var parts = [];
+                 [['products', 'product'], ['pages', 'page'], ['ads', 'ad'], ['discounts', 'discount code']].forEach(function(entry) {
+                     var count = parseInt(button.dataset[entry[0]] || '0', 10);
+                     if (count > 0) {
+                         parts.push(count + ' ' + entry[1] + (count === 1 ? '' : 's'));
+                     }
+                 });
+                 document.getElementById('delete_account_summary').textContent = parts.length
+                     ? 'This person made ' + parts.join(', ') + '. Nothing gets deleted, it all moves to the person you pick below. Orders and customers stay as they are.'
+                     : 'This person hasn\'t made any products, pages, ads or discount codes. Orders and customers stay as they are.';
+                 var firstAllowed = null;
+                 Array.prototype.forEach.call(select.options, function(option) {
+                     option.disabled = option.value === targetId;
+                     option.hidden = option.value === targetId;
+                     if (!option.disabled && firstAllowed === null) {
+                         firstAllowed = option;
+                     }
+                 });
+                 if (select.selectedOptions.length === 0 || select.selectedOptions[0].disabled) {
+                     select.value = firstAllowed ? firstAllowed.value : '';
+                 }
+                 box.style.display = '';
+                 box.scrollIntoView({ behavior: 'smooth' });
+             });
+         });
+ 
+         document.getElementById('delete-account-form').addEventListener('submit', function(event) {
+             var select = document.getElementById('delete_transfer_to');
+             var receiver = select.selectedOptions.length ? select.selectedOptions[0].textContent : '';
+             if (!confirm('Delete ' + document.getElementById('delete_account_name').textContent + ' and give everything they made to ' + receiver + '?')) {
+                 event.preventDefault();
+             }
+         });
+ 
          document.querySelectorAll('.js-edit-account').forEach(function(button) {
              button.addEventListener('click', function() {
                  document.getElementById('edit-account-title').style.display = '';
⋮ 29 unchanged lines ⋮
ads.php Modified +11 -11
⋮ 20 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          require __DIR__ . '/dashboard-actions/manage-ads.php';
          ActivityLog::record('ads.' . (string) ($_POST['action'] ?? ''));
-         $flashMessage = 'Saved.';
+         $flashMessage = 'Changes saved.';
      }
  }
  
⋮ 49 unchanged lines ⋮
                          data-content="<?= htmlspecialchars($ad['content'], ENT_QUOTES) ?>"
                          data-link-url="<?= htmlspecialchars((string) $ad['link_url'], ENT_QUOTES) ?>"
                          data-is-active="<?= (int) $ad['is_active'] ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</button>
-                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this ad?');">
+                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this ad? It will disappear from your store.');">
                          <?= Csrf::field() ?>
                          <input type="hidden" name="action" value="delete_ad">
                          <input type="hidden" name="ad_id" value="<?= (int) $ad['id'] ?>">
⋮ 4 unchanged lines ⋮
          <?php endforeach; ?>
          <?php if (empty($ads)): ?>
              <tr>
-                 <td colspan="5">No ads yet.</td>
+                 <td colspan="5">No ads yet. Use them to promote a sale or show a banner.</td>
              </tr>
          <?php endif; ?>
      </tbody>
⋮ 11 unchanged lines ⋮
  
          <label><?= Icons::icon('ads', 'icon icon-sm') ?>Type</label>
          <select name="type" id="ad_type_select">
-             <option value="static_text" <?= ($editingAd['type'] ?? '') === 'static_text' ? 'selected' : '' ?>>Static — text</option>
-             <option value="static_image" <?= ($editingAd['type'] ?? '') === 'static_image' ? 'selected' : '' ?>>Static — image</option>
-             <option value="script" <?= ($editingAd['type'] ?? '') === 'script' ? 'selected' : '' ?>>Script (HTML / JS)</option>
+             <option value="static_text" <?= ($editingAd['type'] ?? '') === 'static_text' ? 'selected' : '' ?>>Text</option>
+             <option value="static_image" <?= ($editingAd['type'] ?? '') === 'static_image' ? 'selected' : '' ?>>Image</option>
+             <option value="script" <?= ($editingAd['type'] ?? '') === 'script' ? 'selected' : '' ?>>Code from an ad network (HTML or JS)</option>
          </select>
  
-         <label><?= Icons::icon('flag', 'icon icon-sm') ?>Placement — where on the site this ad appears</label>
+         <label><?= Icons::icon('flag', 'icon icon-sm') ?>Where it shows up</label>
          <select name="placement" id="ad_placement_input" required>
              <?php foreach (SiteFront::AD_PLACEMENTS as $placementKey => $placementLabel): ?>
                  <option value="<?= htmlspecialchars($placementKey, ENT_QUOTES) ?>" <?= ($editingAd['placement'] ?? 'header') === $placementKey ? 'selected' : '' ?>><?= htmlspecialchars($placementLabel) ?></option>
              <?php endforeach; ?>
          </select>
  
-         <label id="content_label"><?= Icons::icon('quote', 'icon icon-sm') ?>Content (text, image URL, or raw HTML/JS)</label>
+         <label id="content_label"><?= Icons::icon('quote', 'icon icon-sm') ?>Content (text, an image, or ad code)</label>
          <textarea name="content" id="ad_content_input" rows="6" class="be-code"><?= htmlspecialchars($editingAd['content'] ?? '') ?></textarea>
  
-         <label><?= Icons::icon('external', 'icon icon-sm') ?>Link URL (optional, ignored for script ads)</label>
+         <label><?= Icons::icon('external', 'icon icon-sm') ?>Link (optional, not used for ad code)</label>
          <input type="text" name="link_url" id="ad_link_url_input" value="<?= htmlspecialchars($editingAd['link_url'] ?? '') ?>">
  
-         <label><input type="checkbox" name="is_active" id="ad_is_active_input" <?= ($editingAd['is_active'] ?? 1) ? 'checked' : '' ?>> Active</label>
+         <label><input type="checkbox" name="is_active" id="ad_is_active_input" <?= ($editingAd['is_active'] ?? 1) ? 'checked' : '' ?>> Show it on the store</label>
  
          <div class="publish-actions">
              <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save ad</button>
⋮ 54 unchanged lines ⋮
ai-chat.php Modified +9 -9
⋮ 21 unchanged lines ⋮
  
  if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
      header('Allow: POST');
-     aiRespond(405, ['ok' => false, 'error' => 'Method not allowed.']);
+     aiRespond(405, ['ok' => false, 'error' => 'Something went wrong. Please refresh the page.']);
  }
  
  Auth::boot();
  
  if (!Auth::check()) {
-     aiRespond(401, ['ok' => false, 'error' => 'Your session has expired. Reload the page and sign in again.']);
+     aiRespond(401, ['ok' => false, 'error' => 'You were logged out. Refresh the page and log in again.']);
  }
  
  if (!Csrf::verify($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null)) {
-     aiRespond(403, ['ok' => false, 'error' => 'Security check failed. Reload the page and try again.']);
+     aiRespond(403, ['ok' => false, 'error' => 'Your session expired. Refresh the page and try again.']);
  }
  
  if (!Ai::availableFor(Auth::role())) {
⋮ 2 unchanged lines ⋮
  
  $declaredLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
  if ($declaredLength > AI_MAX_BODY_BYTES) {
-     aiRespond(413, ['ok' => false, 'error' => 'The request is too large.']);
+     aiRespond(413, ['ok' => false, 'error' => 'That\'s too much text at once. Try a shorter message or a smaller part.']);
  }
  
  $rawBody = (string) file_get_contents('php://input', false, null, 0, AI_MAX_BODY_BYTES + 1);
  if (strlen($rawBody) > AI_MAX_BODY_BYTES) {
-     aiRespond(413, ['ok' => false, 'error' => 'The request is too large.']);
+     aiRespond(413, ['ok' => false, 'error' => 'That\'s too much text at once. Try a shorter message or a smaller part.']);
  }
  
  $input = json_decode($rawBody, true);
  if (!is_array($input)) {
-     aiRespond(400, ['ok' => false, 'error' => 'Invalid request.']);
+     aiRespond(400, ['ok' => false, 'error' => 'Something went wrong. Please refresh the page and try again.']);
  }
  
  $message = is_string($input['message'] ?? null) ? trim($input['message']) : '';
  if ($message === '' || mb_strlen($message) > 4000) {
-     aiRespond(400, ['ok' => false, 'error' => 'Write a message of up to 4000 characters.']);
+     aiRespond(400, ['ok' => false, 'error' => 'Please keep your message under 4000 characters.']);
  }
  
  $rawBlocks = is_array($input['blocks'] ?? null) ? array_slice($input['blocks'], 0, 300) : [];
  $blocks = Ai::normalizeBlocks($rawBlocks);
  $fields = Ai::normalizeFields(is_array($input['fields'] ?? null) ? $input['fields'] : []);
  if ($blocks === [] && $fields === []) {
-     aiRespond(400, ['ok' => false, 'error' => 'The article has no blocks to work with.']);
+     aiRespond(400, ['ok' => false, 'error' => 'There\'s nothing written yet for me to work with.']);
  }
  
  $scope = ($input['scope'] ?? 'article') === 'block' ? 'block' : 'article';
⋮ 5 unchanged lines ⋮
  
  try {
      if (Ai::rateLimited($userId)) {
-         aiRespond(429, ['ok' => false, 'error' => 'You reached the hourly limit of AI requests. Try again later.']);
+         aiRespond(429, ['ok' => false, 'error' => 'You\'ve used up this hour\'s AI messages. Try again a bit later.']);
      }
      Ai::recordRequest($userId);
  } catch (Throwable $exception) {
⋮ 17 unchanged lines ⋮
analytics.php Modified +13 -13
⋮ 19 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          require __DIR__ . '/dashboard-actions/manage-analytics.php';
          ActivityLog::record('analytics.' . (string) ($_POST['action'] ?? ''));
-         $flashMessage = 'Saved.';
+         $flashMessage = 'Changes saved.';
      }
  }
  
⋮ 33 unchanged lines ⋮
  <form method="post">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="save_consent_settings">
- <label><input type="checkbox" name="consent_manager_enabled" <?= $consentEnabled ? 'checked' : '' ?>> Enable consent manager (controls whether consent-gated scripts run before the visitor accepts)</label>
- <label><input type="checkbox" name="consent_footer_icon_enabled" <?= $consentFooterIconEnabled ? 'checked' : '' ?>> Show cookie icon in footer (lets visitors reopen the consent manager)</label>
+ <label><input type="checkbox" name="consent_manager_enabled" <?= $consentEnabled ? 'checked' : '' ?>> Ask visitors for cookie consent (scripts marked below only run after they agree)</label>
+ <label><input type="checkbox" name="consent_footer_icon_enabled" <?= $consentFooterIconEnabled ? 'checked' : '' ?>> Show a cookie icon in the footer so visitors can change their mind</label>
  <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
  </form>
  </div>
⋮ 9 unchanged lines ⋮
  <td><?= $script['is_active'] ? 'Yes' : 'No' ?></td>
  <td class="dash-table-actions">
  <a href="analytics.php?edit=<?= (int) $script['id'] ?>" class="dash-btn-small"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</a>
- <form method="post" style="display:inline;" onsubmit="return confirm('Delete this script?');">
+ <form method="post" style="display:inline;" onsubmit="return confirm('Delete this script? It will stop running on your store.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="delete_analytics_script">
  <input type="hidden" name="script_id" value="<?= (int) $script['id'] ?>">
⋮ 3 unchanged lines ⋮
  </tr>
  <?php endforeach; ?>
  <?php if (empty($scripts)): ?>
- <tr><td colspan="5">No analytics scripts yet.</td></tr>
+ <tr><td colspan="5">No tracking scripts yet. Add one for Google Analytics, a Meta pixel and so on.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 9 unchanged lines ⋮
  <label>Name</label>
  <input type="text" name="name" value="<?= htmlspecialchars($editingScript['name'] ?? '') ?>" required>
  
- <label>Script code (raw HTML / &lt;style&gt; / &lt;script&gt;)</label>
+ <label>Code (paste it exactly as the service gives it to you)</label>
  <textarea name="script_code" rows="8" class="be-code"><?= htmlspecialchars($editingScript['script_code'] ?? '') ?></textarea>
  
- <label>Placement</label>
+ <label>Where to add it</label>
  <select name="placement">
- <option value="head" <?= ($editingScript['placement'] ?? 'head') === 'head' ? 'selected' : '' ?>>&lt;head&gt;</option>
- <option value="body_start" <?= ($editingScript['placement'] ?? '') === 'body_start' ? 'selected' : '' ?>>Start of &lt;body&gt;</option>
- <option value="body_end" <?= ($editingScript['placement'] ?? '') === 'body_end' ? 'selected' : '' ?>>End of &lt;body&gt;</option>
+ <option value="head" <?= ($editingScript['placement'] ?? 'head') === 'head' ? 'selected' : '' ?>>Page head (&lt;head&gt;, most common)</option>
+ <option value="body_start" <?= ($editingScript['placement'] ?? '') === 'body_start' ? 'selected' : '' ?>>Top of the page (&lt;body&gt; start)</option>
+ <option value="body_end" <?= ($editingScript['placement'] ?? '') === 'body_end' ? 'selected' : '' ?>>Bottom of the page (&lt;body&gt; end)</option>
  </select>
  
- <label><input type="checkbox" name="requires_consent" <?= ($editingScript['requires_consent'] ?? 1) ? 'checked' : '' ?>> Requires visitor consent before running</label>
- <label><input type="checkbox" name="is_active" <?= ($editingScript['is_active'] ?? 1) ? 'checked' : '' ?>> Active</label>
+ <label><input type="checkbox" name="requires_consent" <?= ($editingScript['requires_consent'] ?? 1) ? 'checked' : '' ?>> Only run after the visitor accepts cookies</label>
+ <label><input type="checkbox" name="is_active" <?= ($editingScript['is_active'] ?? 1) ? 'checked' : '' ?>> Turned on</label>
  
  <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save script</button>
  </form>
⋮ 3 unchanged lines ⋮
assets/ai-chat.js Modified +16 -16
⋮ 81 unchanged lines ⋮
      return window.stocketbaseBlockEditor || null;
    }
  
-   var FIELD_LABELS = { title: 'title', excerpt: 'excerpt', seo_title: 'SEO title', seo_description: 'SEO description' };
+   var FIELD_LABELS = { title: 'title', excerpt: 'short summary', seo_title: 'Google title', seo_description: 'Google description' };
  
    function fieldElement(name) {
      var target = editor();
⋮ 148 unchanged lines ⋮
  
    function undoEntry(entry) {
      if (changes[changes.length - 1] !== entry) {
-       addMessage('note', 'Undo the newer AI changes first.');
+       addMessage('note', 'Undo the newer changes first.');
        return;
      }
  
⋮ 8 unchanged lines ⋮
        return input !== null && String(input.value || '') !== entry.fieldsAfter[name];
      });
  
-     if ((JSON.stringify(target.getBlocks()) !== entry.after || fieldsEdited) && !window.confirm('The article was edited after this AI change. Undoing it will also discard those edits. Continue?')) {
+     if ((JSON.stringify(target.getBlocks()) !== entry.after || fieldsEdited) && !window.confirm('You made edits after this change. Undoing it will throw those edits away too. Go ahead?')) {
        return;
      }
  
⋮ 70 unchanged lines ⋮
        }
  
        if (typeof active.replaceBlocks !== 'function') {
-         status.textContent = 'Reload the page (Ctrl+F5) to load the updated editor.';
+         status.textContent = 'Please refresh the page (Ctrl+F5) and try again.';
          return;
        }
  
⋮ 13 unchanged lines ⋮
        var blocksChanged = JSON.stringify(next) !== JSON.stringify(before);
  
        if (!blocksChanged && Object.keys(fieldsAfter).length === 0) {
-         status.textContent = 'Nothing to change.';
+         status.textContent = 'Nothing needed changing.';
          applyButton.style.display = 'none';
          discardButton.style.display = 'none';
          return;
⋮ 68 unchanged lines ⋮
  
      var target = editor();
      if (!target) {
-       addMessage('error', 'The editor is not ready yet.');
+       addMessage('error', 'The editor is still loading. Give it a second.');
        return;
      }
  
⋮ 3 unchanged lines ⋮
  
      if (scope === 'block') {
        if (focusId === '') {
-         addMessage('error', 'Click into a block first, or switch the scope to "Whole article".');
+         addMessage('error', 'Click into a block first, or switch to "Everything".');
          return;
        }
        blocks = blocks.filter(function (block) { return block.id === focusId; });
⋮ 11 unchanged lines ⋮
      })
        .then(function (response) {
          return response.json().catch(function () {
-           return { ok: false, error: 'Unexpected response from the server.' };
+           return { ok: false, error: 'Something went wrong on the server. Please try again.' };
          });
        })
        .then(function (data) {
          setBusy(false);
          if (!data.ok) {
-           addMessage('error', data.error || 'The request failed.');
+           addMessage('error', data.error || 'That didn\'t work. Please try again.');
            return;
          }
  
⋮ 12 unchanged lines ⋮
        })
        .catch(function () {
          setBusy(false);
-         addMessage('error', 'Could not reach the server.');
+         addMessage('error', 'We couldn\'t reach your store. Check your connection.');
        });
    }
  
⋮ 8 unchanged lines ⋮
    function build() {
      ui.fab = el('button', 'ai-fab');
      ui.fab.type = 'button';
-     ui.fab.setAttribute('aria-label', 'Open the AI helper');
+     ui.fab.setAttribute('aria-label', 'Open the AI assistant');
      ui.fab.appendChild(icon('sparkles'));
      ui.fab.appendChild(el('span', null, 'AI'));
  
⋮ 12 unchanged lines ⋮
      var headerActions = el('div', 'ai-header-actions');
      ui.undo = el('button', 'dash-btn-small');
      ui.undo.type = 'button';
-     ui.undo.title = 'Undo the last applied AI change';
+     ui.undo.title = 'Undo the last change';
      ui.undo.appendChild(icon('refresh'));
      ui.undo.appendChild(document.createTextNode('Undo'));
      ui.undo.disabled = true;
      var closeButton = el('button', 'ai-icon-button');
      closeButton.type = 'button';
-     closeButton.setAttribute('aria-label', 'Close the AI helper');
+     closeButton.setAttribute('aria-label', 'Close the AI assistant');
      closeButton.appendChild(icon('x'));
      headerActions.appendChild(ui.undo);
      headerActions.appendChild(closeButton);
⋮ 23 unchanged lines ⋮
      var scopeRow = el('div', 'ai-scope');
      scopeRow.appendChild(el('label', null, 'Scope'));
      ui.scope = el('select');
-     [['article', 'Whole article'], ['block', 'Selected block']].forEach(function (option) {
+     [['article', 'Everything'], ['block', 'Selected block']].forEach(function (option) {
        var node = el('option', null, option[1]);
        node.value = option[0];
        ui.scope.appendChild(node);
⋮ 4 unchanged lines ⋮
      ui.input = el('textarea');
      ui.input.rows = 2;
      ui.input.maxLength = 4000;
-     ui.input.placeholder = 'Ask the AI…';
+     ui.input.placeholder = 'What should I help with?';
      ui.send = el('button', 'dash-btn dash-btn-primary ai-send');
      ui.send.type = 'button';
      ui.send.setAttribute('aria-label', 'Send');
⋮ 12 unchanged lines ⋮
      document.body.appendChild(ui.fab);
      document.body.appendChild(ui.panel);
  
-     addMessage('note', 'Your article text is sent to ' + (config.dataset.provider || 'the AI provider') + '. Changes are only proposals: nothing is applied until you press Apply, and you can undo it afterwards.');
+     addMessage('note', 'Your text is sent to ' + (config.dataset.provider || 'the AI service') + '. You see every suggestion first, nothing changes until you press Apply, and you can always undo.');
  
      ui.fab.addEventListener('click', function () { togglePanel(true); });
      closeButton.addEventListener('click', function () { togglePanel(false); });
⋮ 38 unchanged lines ⋮
assets/block-editor.js Modified

Not shown (binary file or too large to diff).

assets/dashboard.css Modified

Not shown (binary file or too large to diff).

assets/site.css Modified

Not shown (binary file or too large to diff).

category.php Modified +1 -1
⋮ 25 unchanged lines ⋮
  
  if (!$category) {
      http_response_code(404);
-     $pageTitle = 'Not found - ' . $settings['site_name'];
+     $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
      require __DIR__ . '/includes/front-header.php';
      echo '<p>' . htmlspecialchars(Language::get('category_not_found', 'Category not found.')) . '</p>';
      require __DIR__ . '/includes/front-footer.php';
⋮ 57 unchanged lines ⋮
checkout.php Modified +2 -2
⋮ 66 unchanged lines ⋮
  
      if (!$singleProduct) {
          http_response_code(404);
-         $pageTitle = 'Not found - ' . $settings['site_name'];
+         $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
          require __DIR__ . '/includes/front-header.php';
          echo '<p>' . htmlspecialchars(Language::get('product_not_found', 'Product not found.')) . '</p>';
          require __DIR__ . '/includes/front-footer.php';
⋮ 254 unchanged lines ⋮
                  $stripe = new StripeClient();
                  $couponId = null;
                  if ($discountCents > 0) {
-                     $couponId = $stripe->createOneTimeCoupon($discountCents, $cartCurrency, 'Discount ' . (string) $appliedCode);
+                     $couponId = $stripe->createOneTimeCoupon($discountCents, $cartCurrency, Language::get('checkout_discount_name', 'Discount') . ' ' . (string) $appliedCode);
                  }
  
                  if ($discountCents > 0 && $couponId === null) {
⋮ 151 unchanged lines ⋮
dashboard-actions/delete-page.php Modified +1 -1
⋮ 18 unchanged lines ⋮
      return;
  }
  
- $statement = $db->prepare('DELETE FROM pages WHERE id = :id');
+ $statement = $db->prepare('DELETE FROM pages WHERE id = :id AND is_home = 0');
  $statement->execute(['id' => $pageId]);
  
dashboard-actions/product-files.php Modified +5 -5
⋮ 15 unchanged lines ⋮
  $fileProduct = $productStatement->fetch();
  
  if (!$fileProduct) {
-     return ['Product not found.', 'error', null];
+     return ['We couldn\'t find that product.', 'error', null];
  }
  
  $canManageFiles = Auth::role() !== Auth::ROLE_CATALOG_ASSISTANT
      && ((int) $fileProduct['created_by'] === (int) $currentUser['id'] || Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER));
  
  if (!$canManageFiles) {
-     return ['You do not have permission to manage files for this product.', 'error', null];
+     return ['You don\'t have access to this product\'s files.', 'error', null];
  }
  
  if ($action === 'upload_product_file') {
⋮ 14 unchanged lines ⋮
          }
      }
  
-     return ['Button texts saved.', 'success', $productId];
+     return ['Button text saved.', 'success', $productId];
  }
  
  if ($action === 'delete_product_file') {
      $fileId = (int) ($_POST['file_id'] ?? 0);
      $file = DigitalFiles::find($fileId);
      if ($file === null || (int) $file['product_id'] !== $productId) {
-         return ['File not found.', 'error', $productId];
+         return ['We couldn\'t find that file.', 'error', $productId];
      }
  
      DigitalFiles::delete($fileId);
⋮ 2 unchanged lines ⋮
      return ['File deleted.', 'success', $productId];
  }
  
- return ['Unknown action.', 'error', $productId];
+ return ['Something went wrong. Please try again.', 'error', $productId];
  
dashboard-actions/refund-order.php Modified +7 -7
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     return ['You do not have permission to refund orders.', 'error', null];
+     return ['You don\'t have access to refunds.', 'error', null];
  }
  
  $orderId = (int) ($_POST['order_id'] ?? 0);
  if ($orderId <= 0) {
-     return ['Invalid order.', 'error', null];
+     return ['We couldn\'t find that order.', 'error', null];
  }
  
  $usersDb = Database::users();
⋮ 2 unchanged lines ⋮
  $order = $statement->fetch();
  
  if (!$order) {
-     return ['Order not found.', 'error', null];
+     return ['We couldn\'t find that order.', 'error', null];
  }
  
  if ($order['status'] === 'refunded') {
-     return ['This order has already been refunded.', 'error', $orderId];
+     return ['This order was already refunded.', 'error', $orderId];
  }
  
  if (empty($order['provider_payment_id'])) {
-     return ['This order has no recorded payment to refund.', 'error', null];
+     return ['This order hasn\'t been paid, so there\'s nothing to refund.', 'error', null];
  }
  
  $refunded = false;
⋮ 8 unchanged lines ⋮
  }
  
  if (!$refunded) {
-     return ['The refund could not be processed with the payment provider. No changes were made.', 'error', null];
+     return ['The payment company didn\'t accept the refund, so nothing changed. Please try again or refund it from their site.', 'error', null];
  }
  
  $usersDb->prepare("UPDATE orders SET status = 'refunded', updated_at = NOW() WHERE id = :id")->execute(['id' => $orderId]);
⋮ 4 unchanged lines ⋮
      "INSERT INTO order_status_history (order_id, status, note, changed_by_team_account_id) VALUES (:id, 'refunded', 'Refunded by staff', :account_id)"
  )->execute(['id' => $orderId, 'account_id' => $currentUser['id']]);
  
- return ['Order refunded.', 'success', $orderId];
+ return ['Refund done. The money is on its way back to the customer.', 'success', $orderId];
  
dashboard-actions/restore-version.php Modified +5 -5
⋮ 10 unchanged lines ⋮
  $versionId = (int) ($_POST['version_id'] ?? 0);
  
  if ($productId <= 0 || $versionId <= 0) {
-     return ['Version not found.', 'error', null];
+     return ['We couldn\'t find that version.', 'error', null];
  }
  
  $statement = Database::site()->prepare('SELECT created_by, deleted_at FROM products WHERE id = :id LIMIT 1');
⋮ 1 unchanged line ⋮
  $product = $statement->fetch();
  
  if (!$product || $product['deleted_at'] !== null) {
-     return ['Product not found.', 'error', null];
+     return ['We couldn\'t find that product.', 'error', null];
  }
  
  $isOwner = (int) $product['created_by'] === (int) $currentUser['id'];
  if (!$isOwner && !Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER)) {
-     return ['You do not have permission to restore versions of this product.', 'error', null];
+     return ['You don\'t have access to this product\'s history.', 'error', null];
  }
  
  if (!ProductVersions::restore($productId, $versionId, (int) $currentUser['id'], (string) $currentUser['display_name'])) {
-     return ['Version not found.', 'error', null];
+     return ['We couldn\'t find that version.', 'error', null];
  }
  
  ActivityLog::record('product.restore_version', 'product', $productId, 'version #' . $versionId);
  
- return ['Version restored.', 'success', $productId];
+ return ['Earlier version restored.', 'success', $productId];
  
dashboard-actions/save-discount.php Modified +6 -6
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/currency.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     return ['You do not have permission to manage discounts.', 'error', false];
+     return ['You don\'t have access to discounts.', 'error', false];
  }
  
  $db = Database::site();
⋮ 17 unchanged lines ⋮
  $isActive = isset($_POST['is_active']) ? 1 : 0;
  
  if ($code === '') {
-     return ['A discount code is required.', 'error', false];
+     return ['Please type a code for the discount.', 'error', false];
  }
  
  try {
⋮ 15 unchanged lines ⋮
              'id' => $discountId,
          ]);
  
-         return ['Discount updated.', 'success', true];
+         return ['Discount saved.', 'success', true];
      }
  
      $insert = $db->prepare(
⋮ 12 unchanged lines ⋮
          'created_by' => $currentUser['id'],
      ]);
  
-     return ['Discount created.', 'success', true];
+     return ['Discount created. Customers can use it now.', 'success', true];
  } catch (PDOException $exception) {
      if ((int) $exception->getCode() === 23000 || str_contains($exception->getMessage(), 'uq_discount_code')) {
-         return ['That discount code is already in use. Choose a different code.', 'error', false];
+         return ['That code is taken. Try a different one.', 'error', false];
      }
  
      error_log('StocketBase: could not save discount: ' . $exception->getMessage());
  
-     return ['Could not save the discount. Please try again.', 'error', false];
+     return ['Something went wrong while saving. Please try again.', 'error', false];
  }
  
dashboard-actions/save-nav.php Modified +1 -1
⋮ 76 unchanged lines ⋮
          if ($refId <= 0) {
              return;
          }
-         $exists = $db->prepare('SELECT id FROM pages WHERE id = :id LIMIT 1');
+         $exists = $db->prepare('SELECT id FROM pages WHERE id = :id AND is_home = 0 LIMIT 1');
          $exists->execute(['id' => $refId]);
          if (!$exists->fetch()) {
              return;
⋮ 100 unchanged lines ⋮
dashboard-actions/save-order-status.php Modified +6 -6
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     return ['You do not have permission to update orders.', 'error', null];
+     return ['You don\'t have access to orders.', 'error', null];
  }
  
  $orderId = (int) ($_POST['order_id'] ?? 0);
⋮ 2 unchanged lines ⋮
  $trackingNumber = trim((string) ($_POST['tracking_number'] ?? ''));
  $trackingUrl = trim((string) ($_POST['tracking_url'] ?? ''));
  if ($trackingUrl !== '' && (!preg_match('#^https?://#i', $trackingUrl) || filter_var($trackingUrl, FILTER_VALIDATE_URL) === false)) {
-     return ['Tracking URL must be a valid http(s) link.', 'error', $orderId > 0 ? $orderId : null];
+     return ['The tracking link should start with http:// or https://.', 'error', $orderId > 0 ? $orderId : null];
  }
  $trackingNumber = mb_substr($trackingNumber, 0, 120);
  $note = mb_substr($note, 0, 2000);
  $note = trim((string) ($_POST['note'] ?? ''));
  
  if ($orderId <= 0 || $status === null) {
-     return ['Invalid order or status.', 'error', null];
+     return ['Something doesn\'t look right with that order. Please try again.', 'error', null];
  }
  
  $usersDb = Database::users();
⋮ 2 unchanged lines ⋮
  $order = $statement->fetch();
  
  if (!$order) {
-     return ['Order not found.', 'error', null];
+     return ['We couldn\'t find that order.', 'error', null];
  }
  
  $fulfilledAt = $order['fulfilled_at'];
⋮ 51 unchanged lines ⋮
              Mailer::send($recipientEmail, $email['subject'], $email['html']);
          } catch (Throwable $mailException) {
              error_log('StocketBase: order status email failed — ' . $mailException->getMessage());
-             return ['Order status updated, but the notification email could not be sent.', 'error', $orderId];
+             return ['Status saved, but the email to the customer didn\'t go out.', 'error', $orderId];
          }
      }
  }
  
- return ['Order status updated.', 'success', $orderId];
+ return ['Status saved.', 'success', $orderId];
  
dashboard-actions/save-page.php Modified +23 -2
⋮ 14 unchanged lines ⋮
  $pageId = (int) ($_POST['page_id'] ?? 0) ?: null;
  $status = ($_POST['status'] ?? 'draft') === 'published' ? 'published' : 'draft';
  $showInMenu = isset($_POST['show_in_menu']) ? 1 : 0;
- $blocksJson = BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), true);
  
  if ($title === '') {
      exit;
⋮ 1 unchanged line ⋮
  
  require_once __DIR__ . '/../includes/slugger.php';
  
- $slug = Slugger::make($title);
  $db = Database::site();
+ $isHomePage = false;
+ 
+ if ($pageId !== null) {
+     $homeCheck = $db->prepare('SELECT is_home FROM pages WHERE id = :id LIMIT 1');
+     $homeCheck->execute(['id' => $pageId]);
+     $isHomePage = (int) $homeCheck->fetchColumn() === 1;
+ }
+ 
+ if ($isHomePage) {
+     $blocksJson = BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), false, true);
+     $statement = $db->prepare('UPDATE pages SET title = :title, content_blocks = :content, status = :status, show_in_menu = 0 WHERE id = :id AND is_home = 1');
+     $statement->execute([
+         'title' => mb_substr($title, 0, 255),
+         'content' => $blocksJson,
+         'status' => 'published',
+         'id' => $pageId,
+     ]);
+ 
+     return;
+ }
+ 
+ $blocksJson = BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), true);
+ $slug = Slugger::make($title);
  
  if ($pageId !== null) {
      $previousMenu = $db->prepare('SELECT show_in_menu FROM pages WHERE id = :id LIMIT 1');
⋮ 36 unchanged lines ⋮
dashboard-actions/save-product.php Modified +5 -5
⋮ 40 unchanged lines ⋮
  $polarProductId = trim((string) ($_POST['polar_product_id'] ?? ''));
  
  if ($title === '') {
-     return ['Product title is required.', 'error', null];
+     return ['Give your product a name first.', 'error', null];
  }
  
  $slug = trim((string) ($_POST['slug'] ?? ''));
⋮ 45 unchanged lines ⋮
      $existing = $existingStatement->fetch();
  
      if (!$existing) {
-         return ['Product not found.', 'error', null];
+         return ['We couldn\'t find that product.', 'error', null];
      }
  
      if ($existing['deleted_at'] !== null) {
⋮ 5 unchanged lines ⋮
      $canProofread = Auth::role() === Auth::ROLE_CATALOG_ASSISTANT;
  
      if (!$isOwner && !$canEditAny && !$canProofread) {
-         return ['You do not have permission to edit this product.', 'error', null];
+         return ['You don\'t have access to this product.', 'error', null];
      }
  
      if ($canProofread && !$isOwner) {
⋮ 1 unchanged line ⋮
              'UPDATE products SET content_blocks = :content, updated_at = NOW() WHERE id = :id'
          );
          $update->execute(['content' => $blocksJson, 'id' => $productId]);
-         return ['Proofreading changes saved.', 'success', $productId];
+         return ['Corrections saved.', 'success', $productId];
      }
  
      $status = $existing['status'];
⋮ 47 unchanged lines ⋮
  
      $syncVariants($productId);
  
-     return ['Product updated.', 'success', $productId];
+     return ['Product saved.', 'success', $productId];
  }
  
  $status = 'draft';
⋮ 48 unchanged lines ⋮
dashboard-actions/security.php Modified +5 -5
⋮ 18 unchanged lines ⋮
      $passwordValid = password_verify((string) ($_POST['password'] ?? ''), (string) $currentUser['password_hash']);
      if (!$passwordValid || !TwoFactor::verify($currentUser, (string) ($_POST['code'] ?? ''))) {
          LoginThrottle::recordFailure('totp_manage', (string) $accountId);
-         return 'The password or the code is not correct.';
+         return 'The password or code isn\'t right.';
      }
  
      LoginThrottle::clear('totp_manage', (string) $accountId);
⋮ 18 unchanged lines ⋮
  if ($action === 'totp_confirm') {
      $secret = (string) ($_SESSION['totp_setup_secret'] ?? '');
      if ($twoFactorEnabled || $secret === '') {
-         return ['Start the setup again.', 'error', false];
+         return ['That took too long. Please start the setup again.', 'error', false];
      }
  
      $wait = LoginThrottle::secondsUntilAllowed('totp_setup', (string) $accountId);
⋮ 5 unchanged lines ⋮
      if ($step === null) {
          LoginThrottle::recordFailure('totp_setup', (string) $accountId);
  
-         return ['That code is not valid. Check the time on your phone and try again.', 'error', false];
+         return ['That code didn\'t work. Make sure the time on your phone is correct and try again.', 'error', false];
      }
  
      LoginThrottle::clear('totp_setup', (string) $accountId);
⋮ 6 unchanged lines ⋮
  
  if ($action === 'totp_disable' || $action === 'totp_regenerate') {
      if (!$twoFactorEnabled) {
-         return ['Two-factor authentication is not enabled.', 'error', false];
+         return ['Two-step login is already off.', 'error', false];
      }
  
      $problem = $confirmIdentity();
⋮ 12 unchanged lines ⋮
      return [null, 'success', true];
  }
  
- return ['Unknown action.', 'error', false];
+ return ['Something went wrong. Please try again.', 'error', false];
  
dashboard-actions/send-test-email.php Modified +4 -4
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/mailer.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER)) {
-     $flashMessage = 'You do not have permission to send test emails.';
+     $flashMessage = 'You don\'t have access to test emails.';
      $flashType = 'error';
  
      return;
⋮ 2 unchanged lines ⋮
  $testRecipient = trim((string) ($currentUser['email'] ?? ''));
  
  if ($testRecipient === '' || filter_var($testRecipient, FILTER_VALIDATE_EMAIL) === false) {
-     $flashMessage = 'Your team account has no valid email address on file — add one in Profile first.';
+     $flashMessage = 'Your account doesn\'t have an email address yet. Ask the Super Admin to add one.';
      $flashType = 'error';
  
      return;
⋮ 9 unchanged lines ⋮
  $sent = Mailer::send($testRecipient, 'Test email from ' . $siteName, $html);
  
  if ($sent) {
-     $flashMessage = 'Test email sent to ' . $testRecipient . '.';
+     $flashMessage = 'Test email sent to ' . $testRecipient . '. Check your inbox.';
      $flashType = 'success';
  } else {
-     $flashMessage = 'Could not send the test email — check the settings and the server error log for details.';
+     $flashMessage = 'The test email didn\'t go out. Check your email settings and try again.';
      $flashType = 'error';
  }
  
dashboard-views/categories.php Modified +6 -6
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER)) {
-     echo '<p>You do not have permission to manage categories.</p>';
+     echo '<p>You don\'t have access to categories.</p>';
      return;
  }
  
⋮ 44 unchanged lines ⋮
                      <?php if (Languages::enabled()): ?>
                      <a href="translate.php?type=category&id=<?= (int) $category['id'] ?>" class="dash-btn-small"><?= Icons::icon('translate', 'icon icon-sm') ?>Translate</a>
                      <?php endif; ?>
-                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this category? Products in it will become uncategorized.');">
+                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this category? Its products stay, they just won\'t have a category.');">
                          <?= Csrf::field() ?>
                          <input type="hidden" name="action" value="delete_category">
                          <input type="hidden" name="category_id" value="<?= (int) $category['id'] ?>">
⋮ 4 unchanged lines ⋮
          <?php endforeach; ?>
          <?php if (empty($categories)): ?>
              <tr>
-                 <td colspan="4">No categories yet.</td>
+                 <td colspan="4">No categories yet. Add one to group your products.</td>
              </tr>
          <?php endif; ?>
      </tbody>
⋮ 12 unchanged lines ⋮
          <label>Description</label>
          <textarea name="description" id="category_description_input" rows="2"></textarea>
  
-         <label>Parent category</label>
+         <label>Put it inside another category (optional)</label>
          <select name="parent_id" id="category_parent_input">
-             <option value="">— none —</option>
+             <option value="">No, keep it on its own</option>
              <?php foreach ($categories as $category): ?>
                  <option value="<?= (int) $category['id'] ?>"><?= htmlspecialchars($category['name']) ?></option>
              <?php endforeach; ?>
          </select>
  
-         <label><input type="checkbox" name="show_in_menu" id="category_show_in_menu_input" checked> Show in site menu</label>
+         <label><input type="checkbox" name="show_in_menu" id="category_show_in_menu_input" checked> Show in the store menu</label>
  
          <div class="publish-actions">
              <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save category</button>
⋮ 49 unchanged lines ⋮
dashboard-views/customer-detail.php Modified +10 -10
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/currency.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to view customers.</p>';
+     echo '<p>You don\'t have access to customers.</p>';
      return;
  }
  
⋮ 5 unchanged lines ⋮
  $customer = $statement->fetch();
  
  if (!$customer) {
-     echo '<p>Customer not found.</p>';
+     echo '<p>We couldn\'t find this customer.</p>';
      return;
  }
  
⋮ 15 unchanged lines ⋮
  
  <div class="sidebar-box" style="max-width:480px;">
  <p>Email: <?= htmlspecialchars($customer['email']) ?><br>
- Status: <span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars($customer['status']) ?></span><br>
+ Status: <span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars(StatusLabel::get((string) $customer['status'])) ?></span><br>
  Joined: <?= htmlspecialchars($customer['created_at']) ?><br>
- Last login: <?= htmlspecialchars((string) ($customer['last_login_at'] ?? '—')) ?></p>
+ Last seen: <?= htmlspecialchars((string) ($customer['last_login_at'] ?? 'Never')) ?></p>
  </div>
  
  <?php if (!empty($subscriptions)): ?>
  <h2 class="dash-subtitle"><?= Icons::icon('credit-card', 'icon icon-sm') ?>Subscriptions</h2>
  <table class="dash-table">
- <thead><tr><th>Provider</th><th>Plan</th><th>Status</th><th>Renews / ends</th></tr></thead>
+ <thead><tr><th>Paid with</th><th>Plan</th><th>Status</th><th>Renews or ends on</th></tr></thead>
  <tbody>
  <?php foreach ($subscriptions as $subscription): ?>
  <tr>
  <td><?= htmlspecialchars($subscription['provider']) ?></td>
- <td><?= htmlspecialchars((string) ($subscription['plan_code'] ?? '—')) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($subscription['status']) ?>"><?= htmlspecialchars($subscription['status']) ?></span></td>
- <td><?= htmlspecialchars((string) ($subscription['current_period_end'] ?? '—')) ?></td>
+ <td><?= htmlspecialchars((string) ($subscription['plan_code'] ?? 'Unknown')) ?></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($subscription['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $subscription['status'])) ?></span></td>
+ <td><?= htmlspecialchars((string) ($subscription['current_period_end'] ?? 'No end date')) ?></td>
  </tr>
  <?php endforeach; ?>
  </tbody>
⋮ 2 unchanged lines ⋮
  
  <h2 class="dash-subtitle"><?= Icons::icon('checklist', 'icon icon-sm') ?>Orders</h2>
  <table class="dash-table">
- <thead><tr><th>Order</th><th>Total</th><th>Status</th><th>Placed</th><th></th></tr></thead>
+ <thead><tr><th>Order</th><th>Total</th><th>Status</th><th>Date</th><th></th></tr></thead>
  <tbody>
  <?php foreach ($orders as $order): ?>
  <tr>
  <td><?= htmlspecialchars($order['order_number']) ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $order['total_cents'], $order['currency'])) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars($order['status']) ?></span></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $order['status'])) ?></span></td>
  <td><?= htmlspecialchars($order['created_at']) ?></td>
  <td><a href="dashboard.php?view=order-detail&id=<?= (int) $order['id'] ?>" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a></td>
  </tr>
⋮ 7 unchanged lines ⋮
dashboard-views/customers.php Modified +5 -5
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/currency.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to view customers.</p>';
+     echo '<p>You don\'t have access to customers.</p>';
      return;
  }
  
⋮ 16 unchanged lines ⋮
  <th><?= Icons::icon('heading', 'icon icon-sm') ?>Name</th>
  <th><?= Icons::icon('user', 'icon icon-sm') ?>Email</th>
  <th><?= Icons::icon('checklist', 'icon icon-sm') ?>Orders</th>
- <th><?= Icons::icon('dollar', 'icon icon-sm') ?>Lifetime value</th>
+ <th><?= Icons::icon('dollar', 'icon icon-sm') ?>Spent in total</th>
  <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  <th></th>
  </tr>
⋮ 1 unchanged line ⋮
  <tbody>
  <?php foreach ($customers as $customer): ?>
  <tr>
- <td><?= htmlspecialchars((string) ($customer['display_name'] ?? '—')) ?></td>
+ <td><?= htmlspecialchars((string) ($customer['display_name'] ?? 'No name')) ?></td>
  <td><?= htmlspecialchars($customer['email']) ?></td>
  <td><?= (int) $customer['order_count'] ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $customer['lifetime_value'], $storeCurrency)) ?></td>
- <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars($customer['status']) ?></span></td>
+ <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars(StatusLabel::get((string) $customer['status'])) ?></span></td>
  <td class="dash-table-actions"><a href="dashboard.php?view=customer-detail&id=<?= (int) $customer['id'] ?>" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a></td>
  </tr>
  <?php endforeach; ?>
⋮ 3 unchanged lines ⋮
  </tbody>
  </table>
  <?php if (Auth::hasRoleAtLeast(Auth::ROLE_SUPER_ADMIN)): ?>
- <p style="font-size:12.5px;color:var(--muted);margin-top:12px;">To suspend or reactivate a customer account, use <a href="admin.php?tab=customers">Team accounts &rarr; Customer accounts</a>.</p>
+ <p style="font-size:12.5px;color:var(--muted);margin-top:12px;">Need to block or unblock someone? Go to <a href="admin.php?tab=customers">Team &rarr; Customers</a>.</p>
  <?php endif; ?>
  
dashboard-views/discounts.php Modified +12 -12
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to manage discounts.</p>';
+     echo '<p>You don\'t have access to discounts.</p>';
      return;
  }
  
⋮ 34 unchanged lines ⋮
      data-min-order="<?= $discount['min_order_cents'] !== null ? number_format(((int) $discount['min_order_cents']) / 100, 2, '.', '') : '' ?>"
      data-max-redemptions="<?= htmlspecialchars((string) ($discount['max_redemptions'] ?? ''), ENT_QUOTES) ?>"
      data-active="<?= (int) $discount['is_active'] ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</button>
- <form method="post" style="display:inline;" onsubmit="return confirm('Delete this discount code?');">
+ <form method="post" style="display:inline;" onsubmit="return confirm('Delete this discount code? Customers won\'t be able to use it anymore.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="delete_discount">
  <input type="hidden" name="discount_id" value="<?= (int) $discount['id'] ?>">
⋮ 3 unchanged lines ⋮
  </tr>
  <?php endforeach; ?>
  <?php if (empty($discounts)): ?>
- <tr><td colspan="5">No discount codes yet.</td></tr>
+ <tr><td colspan="5">No discount codes yet. Create one for a sale or a thank-you gift.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 8 unchanged lines ⋮
  <label>Code</label>
  <input type="text" name="code" id="discount_code_input" required>
  
- <label>Description (optional)</label>
+ <label>Note for yourself (optional)</label>
  <input type="text" name="description" id="discount_description_input">
  
- <label>Type</label>
+ <label>Discount type</label>
  <select name="discount_type" id="discount_type_input">
- <option value="percentage">Percentage off</option>
- <option value="fixed_amount">Fixed amount off</option>
+ <option value="percentage">Percent off (like 10%)</option>
+ <option value="fixed_amount">Money off (like 20 PLN)</option>
  </select>
  
- <label>Value (percent, or amount in major units e.g. 10.00)</label>
+ <label>How much (a percent, or an amount like 10.00)</label>
  <input type="text" name="value" id="discount_value_input" required>
  
- <label>Currency (only used for fixed amount)</label>
+ <label>Currency (only for money off)</label>
  <input type="text" name="currency" id="discount_currency_input" maxlength="3" value="<?= htmlspecialchars(SiteFront::settings()['store_currency'] ?? 'USD') ?>">
  
- <label>Minimum order amount (optional, major units)</label>
+ <label>Minimum order (optional, like 50.00)</label>
  <input type="text" name="min_order" id="discount_min_order_input">
  
- <label>Max redemptions (optional)</label>
+ <label>How many times it can be used (optional)</label>
  <input type="number" name="max_redemptions" id="discount_max_redemptions_input">
  
- <label><input type="checkbox" name="is_active" id="discount_active_input" checked> Active</label>
+ <label><input type="checkbox" name="is_active" id="discount_active_input" checked> Customers can use it now</label>
  
  <div class="publish-actions">
  <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save discount</button>
⋮ 51 unchanged lines ⋮
dashboard-views/order-detail.php Modified +18 -18
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/currency.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to manage orders.</p>';
+     echo '<p>You don\'t have access to orders.</p>';
      return;
  }
  
⋮ 5 unchanged lines ⋮
  $order = $statement->fetch();
  
  if (!$order) {
-     echo '<p>Order not found.</p>';
+     echo '<p>We couldn\'t find this order.</p>';
      return;
  }
  
⋮ 16 unchanged lines ⋮
  <div class="article-form-grid">
  <div class="article-form-main">
  
- <h2 class="dash-subtitle"><?= Icons::icon('grid', 'icon icon-sm') ?>Items</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('grid', 'icon icon-sm') ?>What they ordered</h2>
  <table class="dash-table">
- <thead><tr><th>Product</th><th>Variant</th><th>Qty</th><th>Unit price</th><th>Total</th></tr></thead>
+ <thead><tr><th>Product</th><th>Option</th><th>Quantity</th><th>Price each</th><th>Total</th></tr></thead>
  <tbody>
  <?php foreach ($items as $item): ?>
  <tr>
  <td><?= htmlspecialchars($item['product_title_snapshot']) ?></td>
- <td><?= htmlspecialchars((string) ($item['variant_name_snapshot'] ?? '—')) ?></td>
+ <td><?= htmlspecialchars((string) ($item['variant_name_snapshot'] ?? 'Standard')) ?></td>
  <td><?= (int) $item['quantity'] ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $item['unit_price_cents'], $order['currency'])) ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $item['total_cents'], $order['currency'])) ?></td>
⋮ 30 unchanged lines ⋮
  <p><?= nl2br(htmlspecialchars($order['customer_note'])) ?></p>
  <?php endif; ?>
  
- <h2 class="dash-subtitle"><?= Icons::icon('activity', 'icon icon-sm') ?>Timeline</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('activity', 'icon icon-sm') ?>History</h2>
  <ul style="list-style:none;padding:0;margin:0;display:grid;gap:8px;font-size:13px;">
  <?php foreach ($history as $entry): ?>
- <li><span class="status-pill status-<?= htmlspecialchars($entry['status']) ?>"><?= htmlspecialchars($entry['status']) ?></span> <?= htmlspecialchars($entry['created_at']) ?><?= $entry['note'] ? ' — ' . htmlspecialchars($entry['note']) : '' ?></li>
+ <li><span class="status-pill status-<?= htmlspecialchars($entry['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $entry['status'])) ?></span> <?= htmlspecialchars($entry['created_at']) ?><?= $entry['note'] ? ', ' . htmlspecialchars($entry['note']) : '' ?></li>
  <?php endforeach; ?>
  <?php if (empty($history)): ?>
- <li>No status changes recorded yet.</li>
+ <li>Nothing has changed yet.</li>
  <?php endif; ?>
  </ul>
  
⋮ 3 unchanged lines ⋮
  
  <div class="sidebar-box">
  <label>Customer</label>
- <p><?= htmlspecialchars((string) ($order['display_name'] ?: '—')) ?><br><?= htmlspecialchars((string) ($order['account_email'] ?: $order['guest_email'] ?: '—')) ?></p>
+ <p><?= htmlspecialchars((string) ($order['display_name'] ?: 'Guest')) ?><br><?= htmlspecialchars((string) ($order['account_email'] ?: $order['guest_email'] ?: 'No email')) ?></p>
  <?php if ($order['user_account_id']): ?>
  <a href="dashboard.php?view=customer-detail&id=<?= (int) $order['user_account_id'] ?>" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>View customer</a>
  <?php endif; ?>
⋮ 1 unchanged line ⋮
  
  <div class="sidebar-box">
  <label>Payment</label>
- <p>Provider: <?= htmlspecialchars((string) ($order['provider'] ?: '—')) ?><br>
- Payment ID: <?= htmlspecialchars((string) ($order['provider_payment_id'] ?: '—')) ?></p>
+ <p>Paid with: <?= htmlspecialchars((string) ($order['provider'] ? ucfirst((string) $order['provider']) : 'Not paid yet')) ?><br>
+ Payment reference: <?= htmlspecialchars((string) ($order['provider_payment_id'] ?: 'None yet')) ?></p>
  </div>
  
  <div class="sidebar-box">
- <label>Update status</label>
+ <label>Change status</label>
  <form method="post">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="update_order_status">
  <input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
  <select name="status">
  <?php foreach ($allowedStatuses as $status): ?>
- <option value="<?= htmlspecialchars($status) ?>" <?= $order['status'] === $status ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst($status)) ?></option>
+ <option value="<?= htmlspecialchars($status) ?>" <?= $order['status'] === $status ? 'selected' : '' ?>><?= htmlspecialchars(StatusLabel::get($status)) ?></option>
  <?php endforeach; ?>
  </select>
  <label>Tracking number (optional)</label>
  <input type="text" name="tracking_number" value="<?= htmlspecialchars((string) ($order['tracking_number'] ?? '')) ?>">
- <label>Tracking URL (optional)</label>
+ <label>Tracking link (optional)</label>
  <input type="text" name="tracking_url" value="<?= htmlspecialchars((string) ($order['tracking_url'] ?? '')) ?>">
- <label>Note (optional)</label>
+ <label>Note for the history (optional)</label>
  <input type="text" name="note">
- <label><input type="checkbox" name="notify_customer" checked> Email the customer about this update</label>
+ <label><input type="checkbox" name="notify_customer" checked> Let the customer know by email</label>
  <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:10px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save status</button>
  </form>
  </div>
⋮ 1 unchanged line ⋮
  <?php if (in_array($order['status'], ['paid', 'processing', 'fulfilled', 'shipped', 'completed'], true) && !empty($order['provider_payment_id'])): ?>
  <div class="sidebar-box">
  <label>Refund</label>
- <form method="post" onsubmit="return confirm('Refund this order via ' + <?= json_encode($order['provider']) ?> + '? This cannot be undone.');">
+ <form method="post" onsubmit="return confirm('Refund this order through ' + <?= json_encode($order['provider']) ?> + '? The money goes back to the customer and this can\'t be undone.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="refund_order">
  <input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
- <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Refund order</button>
+ <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Refund</button>
  </form>
  </div>
  <?php endif; ?>
⋮ 4 unchanged lines ⋮
dashboard-views/orders.php Modified +5 -5
⋮ 9 unchanged lines ⋮
  require_once __DIR__ . '/../includes/currency.php';
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to manage orders.</p>';
+     echo '<p>You don\'t have access to orders.</p>';
      return;
  }
  
⋮ 20 unchanged lines ⋮
  <div class="settings-tabs">
  <a href="dashboard.php?view=orders" class="<?= $statusFilter === '' ? 'active' : '' ?>">All</a>
  <?php foreach ($allowedStatuses as $status): ?>
- <a href="dashboard.php?view=orders&status=<?= urlencode($status) ?>" class="<?= $statusFilter === $status ? 'active' : '' ?>"><?= htmlspecialchars(ucfirst($status)) ?></a>
+ <a href="dashboard.php?view=orders&status=<?= urlencode($status) ?>" class="<?= $statusFilter === $status ? 'active' : '' ?>"><?= htmlspecialchars(StatusLabel::get($status)) ?></a>
  <?php endforeach; ?>
  </div>
  
⋮ 12 unchanged lines ⋮
  <?php foreach ($orders as $order): ?>
  <tr>
  <td><?= htmlspecialchars($order['order_number']) ?></td>
- <td><?= htmlspecialchars((string) ($order['account_email'] ?: $order['guest_email'] ?: '—')) ?></td>
+ <td><?= htmlspecialchars((string) ($order['account_email'] ?: $order['guest_email'] ?: 'No email')) ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $order['total_cents'], $order['currency'])) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars($order['status']) ?></span></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $order['status'])) ?></span></td>
  <td><?= htmlspecialchars($order['created_at']) ?></td>
  <td class="dash-table-actions"><a href="dashboard.php?view=order-detail&id=<?= (int) $order['id'] ?>" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a></td>
  </tr>
  <?php endforeach; ?>
  <?php if (empty($orders)): ?>
- <tr><td colspan="6">No orders found.</td></tr>
+ <tr><td colspan="6">No orders here yet.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 1 unchanged line ⋮
dashboard-views/overview.php Modified +16 -16
⋮ 31 unchanged lines ⋮
  
  $recentStatement = $db->prepare(
      Auth::isAdministrative()
-         ? 'SELECT p.*, t.display_name FROM products p JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NULL ORDER BY p.updated_at DESC LIMIT 8'
-         : 'SELECT p.*, t.display_name FROM products p JOIN team_accounts t ON t.id = p.created_by WHERE p.created_by = :id AND p.deleted_at IS NULL ORDER BY p.updated_at DESC LIMIT 8'
+         ? 'SELECT p.*, t.display_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NULL ORDER BY p.updated_at DESC LIMIT 8'
+         : 'SELECT p.*, t.display_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by WHERE p.created_by = :id AND p.deleted_at IS NULL ORDER BY p.updated_at DESC LIMIT 8'
  );
  if (!Auth::isAdministrative()) {
      $recentStatement->execute(['id' => $currentUser['id']]);
⋮ 27 unchanged lines ⋮
  
  <div class="dash-cards">
  <?php if ($isManager): ?>
- <div class="dash-card"><?= Icons::icon('dollar', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenueToday, $storeCurrency)) ?></div><div class="dash-card-label">Revenue today</div></div>
- <div class="dash-card"><?= Icons::icon('credit-card', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenue7d, $storeCurrency)) ?></div><div class="dash-card-label">Revenue (7 days)</div></div>
- <div class="dash-card"><?= Icons::icon('stats', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenue30d, $storeCurrency)) ?></div><div class="dash-card-label">Revenue (30 days)</div></div>
- <div class="dash-card"><?= Icons::icon('checklist', 'icon icon-lg') ?><div class="dash-card-value"><?= $ordersPending ?></div><div class="dash-card-label">Orders awaiting payment</div></div>
+ <div class="dash-card"><?= Icons::icon('dollar', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenueToday, $storeCurrency)) ?></div><div class="dash-card-label">Sales today</div></div>
+ <div class="dash-card"><?= Icons::icon('credit-card', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenue7d, $storeCurrency)) ?></div><div class="dash-card-label">Sales, last 7 days</div></div>
+ <div class="dash-card"><?= Icons::icon('stats', 'icon icon-lg') ?><div class="dash-card-value"><?= htmlspecialchars(Currency::format($revenue30d, $storeCurrency)) ?></div><div class="dash-card-label">Sales, last 30 days</div></div>
+ <div class="dash-card"><?= Icons::icon('checklist', 'icon icon-lg') ?><div class="dash-card-value"><?= $ordersPending ?></div><div class="dash-card-label">Orders not paid yet</div></div>
  <?php endif; ?>
  <?php if (Auth::isAdministrative()): ?>
- <div class="dash-card"><div class="dash-card-value"><?= $publishedCount ?></div><div class="dash-card-label">Published products</div></div>
- <div class="dash-card"><div class="dash-card-value"><?= $pendingCount ?></div><div class="dash-card-label">Pending review</div></div>
+ <div class="dash-card"><?= Icons::icon('grid', 'icon icon-lg') ?><div class="dash-card-value"><?= $publishedCount ?></div><div class="dash-card-label">Products in your store</div></div>
+ <div class="dash-card"><?= Icons::icon('clock', 'icon icon-lg') ?><div class="dash-card-value"><?= $pendingCount ?></div><div class="dash-card-label">Waiting for your approval</div></div>
  <?php else: ?>
- <div class="dash-card"><div class="dash-card-value"><?= $myProductCount ?></div><div class="dash-card-label">Your products</div></div>
- <div class="dash-card"><div class="dash-card-value"><?= $publishedCount ?></div><div class="dash-card-label">Published</div></div>
+ <div class="dash-card"><?= Icons::icon('edit', 'icon icon-lg') ?><div class="dash-card-value"><?= $myProductCount ?></div><div class="dash-card-label">Your products</div></div>
+ <div class="dash-card"><?= Icons::icon('grid', 'icon icon-lg') ?><div class="dash-card-value"><?= $publishedCount ?></div><div class="dash-card-label">Live in the store</div></div>
  <?php endif; ?>
  </div>
  
⋮ 6 unchanged lines ⋮
  <tr>
  <td><?= htmlspecialchars($order['order_number']) ?></td>
  <td><?= htmlspecialchars(Currency::format((int) $order['total_cents'], $order['currency'])) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars($order['status']) ?></span></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($order['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $order['status'])) ?></span></td>
  <td><?= htmlspecialchars($order['created_at']) ?></td>
  <td><a href="dashboard.php?view=order-detail&id=<?= (int) $order['id'] ?>" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a></td>
  </tr>
⋮ 5 unchanged lines ⋮
  </table>
  
  <?php if (!empty($lowStockProducts)): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('flag', 'icon icon-sm') ?>Low stock</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('flag', 'icon icon-sm') ?>Running low</h2>
  <table class="dash-table">
  <thead><tr><th><?= Icons::icon('heading', 'icon icon-sm') ?>Product</th><th><?= Icons::icon('grid', 'icon icon-sm') ?>Stock left</th><th></th></tr></thead>
  <tbody>
⋮ 9 unchanged lines ⋮
  <?php endif; ?>
  <?php endif; ?>
  
- <h2 class="dash-subtitle"><?= Icons::icon('activity', 'icon icon-sm') ?>Recent products</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('activity', 'icon icon-sm') ?>Latest product changes</h2>
  <table class="dash-table">
- <thead><tr><th><?= Icons::icon('heading', 'icon icon-sm') ?>Title</th><th><?= Icons::icon('user', 'icon icon-sm') ?>Created by</th><th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th><th><?= Icons::icon('clock', 'icon icon-sm') ?>Updated</th></tr></thead>
+ <thead><tr><th><?= Icons::icon('heading', 'icon icon-sm') ?>Title</th><th><?= Icons::icon('user', 'icon icon-sm') ?>Added by</th><th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th><th><?= Icons::icon('clock', 'icon icon-sm') ?>Updated</th></tr></thead>
  <tbody>
  <?php foreach ($recentProducts as $product): ?>
  <tr>
  <td><a href="dashboard.php?view=product-edit&id=<?= (int) $product['id'] ?>"><?= htmlspecialchars($product['title']) ?></a></td>
- <td><?= htmlspecialchars($product['display_name']) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars($product['status']) ?></span></td>
+ <td><?= htmlspecialchars((string) ($product['display_name'] ?? 'System')) ?></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $product['status'])) ?></span></td>
  <td><?= htmlspecialchars($product['updated_at']) ?></td>
  </tr>
  <?php endforeach; ?>
⋮ 6 unchanged lines ⋮
dashboard-views/pages.php Modified +31 -12
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to manage pages.</p>';
+     echo '<p>You don\'t have access to pages.</p>';
      return;
  }
  
  $db = Database::site();
  
+ try {
+     SiteFront::homePage();
+ } catch (Throwable $exception) {
+ }
+ 
  $pageId = (int) ($_GET['page_id'] ?? 0) ?: null;
  $editingPage = null;
  $showForm = $pageId !== null || isset($_GET['new']);
⋮ 4 unchanged lines ⋮
      $editingPage = $statement->fetch() ?: null;
  }
  
- $pages = $db->query('SELECT * FROM pages ORDER BY sort_order ASC, title ASC')->fetchAll();
+ $pages = $db->query('SELECT * FROM pages ORDER BY is_home DESC, sort_order ASC, title ASC')->fetchAll();
+ $isHomeEditing = $editingPage !== null && (int) ($editingPage['is_home'] ?? 0) === 1;
  $blocksJson = $editingPage['content_blocks'] ?? '{"blocks":[]}';
+ if ($isHomeEditing) {
+     $blocksJson = BlockEditor::sanitize((string) $blocksJson, false, true);
+ }
  
  ?>
  <div class="dash-header-row">
⋮ 7 unchanged lines ⋮
  <thead><tr><th><?= Icons::icon('heading', 'icon icon-sm') ?>Title</th><th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th><th><?= Icons::icon('menu', 'icon icon-sm') ?>In menu</th><th></th></tr></thead>
  <tbody>
  <?php foreach ($pages as $page): ?>
- <tr>
- <td><?= htmlspecialchars($page['title']) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($page['status']) ?>"><?= htmlspecialchars($page['status']) ?></span></td>
- <td><?= $page['show_in_menu'] ? 'Yes' : 'No' ?></td>
+ <?php $isHomeRow = (int) ($page['is_home'] ?? 0) === 1; ?>
+ <tr<?= $isHomeRow ? ' class="page-row-home"' : '' ?>>
+ <td><?php if ($isHomeRow): ?><?= Icons::icon('home', 'icon icon-sm') ?> <?php endif; ?><?= htmlspecialchars($page['title']) ?><?php if ($isHomeRow): ?> <span class="status-pill status-published">Home page</span><?php endif; ?></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($page['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $page['status'])) ?></span></td>
+ <td><?= $isHomeRow ? 'Always' : ($page['show_in_menu'] ? 'Yes' : 'No') ?></td>
  <td class="dash-table-actions">
- <a href="dashboard.php?view=pages&page_id=<?= (int) $page['id'] ?>" class="dash-btn-small"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</a>
+ <a href="dashboard.php?view=pages&page_id=<?= (int) $page['id'] ?>" class="dash-btn-small"><?= Icons::icon('edit', 'icon icon-sm') ?><?= $isHomeRow ? 'Design' : 'Edit' ?></a>
  <?php if (Languages::enabled()): ?>
  <a href="translate.php?type=page&id=<?= (int) $page['id'] ?>" class="dash-btn-small"><?= Icons::icon('translate', 'icon icon-sm') ?>Translate</a>
  <?php endif; ?>
- <form method="post" style="display:inline;" onsubmit="return confirm('Delete this page?');">
+ <?php if (!$isHomeRow): ?>
+ <form method="post" style="display:inline;" onsubmit="return confirm('Delete this page? This can\'t be undone.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="delete_page">
  <input type="hidden" name="page_id" value="<?= (int) $page['id'] ?>">
  <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  </form>
+ <?php endif; ?>
  </td>
  </tr>
  <?php endforeach; ?>
  <?php if (empty($pages)): ?>
- <tr><td colspan="4">No pages yet.</td></tr>
+ <tr><td colspan="4">No pages yet. Add one for things like About us or Shipping info.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
  
  <?php if ($showForm): ?>
  <div class="sidebar-box" style="margin-top:20px;">
- <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('pages', 'icon icon-sm') ?><?= $editingPage ? 'Edit page' : 'New page' ?></h2>
+ <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon($isHomeEditing ? 'home' : 'pages', 'icon icon-sm') ?><?= $isHomeEditing ? 'Design home page' : ($editingPage ? 'Edit page' : 'New page') ?></h2>
+ <?php if ($isHomeEditing): ?>
+ <p style="color:var(--muted);margin:0 0 14px;">This is your store's front page. <strong>Featured products</strong> and <strong>Product list</strong> always stay on it. You can move them up or down, and add anything you like around them. The title is only for you, customers won't see it.</p>
+ <?php endif; ?>
  
  <form method="post" id="page-form">
  <?= Csrf::field() ?>
⋮ 7 unchanged lines ⋮
  <input type="text" name="title" value="<?= htmlspecialchars($editingPage['title'] ?? '') ?>" required>
  
  <label><?= Icons::icon('quote', 'icon icon-sm') ?>Content</label>
+ <?php if ($isHomeEditing): ?>
+ <div id="block-editor-root" data-initial-blocks='<?= htmlspecialchars($blocksJson, ENT_QUOTES) ?>' data-home="1"></div>
+ <?php else: ?>
  <div id="block-editor-root" data-initial-blocks='<?= htmlspecialchars($blocksJson, ENT_QUOTES) ?>'<?= (SiteFront::settings()['contact_enabled'] ?? '0') === '1' ? ' data-allow-contact="1"' : '' ?>></div>
  
- <label><input type="checkbox" name="show_in_menu" <?= !empty($editingPage['show_in_menu']) ? 'checked' : '' ?>> Show in site menu</label>
+ <label><input type="checkbox" name="show_in_menu" <?= !empty($editingPage['show_in_menu']) ? 'checked' : '' ?>> Show in the store menu</label>
  
  <label><?= Icons::icon('flag', 'icon icon-sm') ?>Status</label>
  <select name="status">
  <option value="draft" <?= ($editingPage['status'] ?? 'draft') === 'draft' ? 'selected' : '' ?>>Draft</option>
- <option value="published" <?= ($editingPage['status'] ?? '') === 'published' ? 'selected' : '' ?>>Published</option>
+ <option value="published" <?= ($editingPage['status'] ?? '') === 'published' ? 'selected' : '' ?>>Live</option>
  </select>
+ <?php endif; ?>
  
  <div class="publish-actions">
  <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save page</button>
⋮ 10 unchanged lines ⋮
dashboard-views/product-edit.php Modified +39 -39
⋮ 20 unchanged lines ⋮
      $product = $statement->fetch();
  
      if (!$product) {
-         echo '<p>Product not found.</p>';
+         echo '<p>We couldn\'t find this product. It may have been deleted.</p>';
          return;
      }
  
⋮ 2 unchanged lines ⋮
      $canProofread = Auth::role() === Auth::ROLE_CATALOG_ASSISTANT;
  
      if (!$isOwner && !$canEditAny && !$canProofread) {
-         echo '<p>You do not have permission to view this product.</p>';
+         echo '<p>You don\'t have access to this product.</p>';
          return;
      }
  
      if ($product['deleted_at'] !== null) {
-         echo '<div class="dash-flash dash-flash-error">This product is in the trash.</div>';
+         echo '<div class="dash-flash dash-flash-error">This product is in the trash. Restore it to make changes.</div>';
          if ($isOwner || $canEditAny) {
              echo '<form method="post">' . Csrf::field()
                  . '<input type="hidden" name="action" value="restore_product">'
⋮ 33 unchanged lines ⋮
  <?php if ($product['status'] === 'published' && !empty($product['slug'])): ?>
  <a href="<?= htmlspecialchars(SiteFront::productUrl($product['slug'])) ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a>
  <?php else: ?>
- <a href="product.php?preview=<?= (int) $product['id'] ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>View in preview mode</a>
+ <a href="product.php?preview=<?= (int) $product['id'] ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>Preview</a>
  <?php endif; ?>
  <?php endif; ?>
  </div>
⋮ 12 unchanged lines ⋮
  <label><?= Icons::icon('heading', 'icon icon-sm') ?>Title</label>
  <input type="text" name="title" value="<?= htmlspecialchars($product['title'] ?? '') ?>" <?= $readOnlyMeta ? 'readonly' : '' ?> required>
  
- <label><?= Icons::icon('quote', 'icon icon-sm') ?>Short description</label>
+ <label><?= Icons::icon('quote', 'icon icon-sm') ?>Short summary (shown on product cards)</label>
  <textarea name="excerpt" rows="2" <?= $readOnlyMeta ? 'readonly' : '' ?>><?= htmlspecialchars($product['excerpt'] ?? '') ?></textarea>
  
  <label><?= Icons::icon('grid', 'icon icon-sm') ?>Full description</label>
⋮ 1 unchanged line ⋮
  
  <?php if (!$readOnlyMeta): ?>
  <div class="sidebar-box">
- <label><?= Icons::icon('layers', 'icon icon-sm') ?>Variants (optional — e.g. size or color, one per row)</label>
+ <label><?= Icons::icon('layers', 'icon icon-sm') ?>Variants (optional, like sizes or colors, one per row)</label>
  <table class="dash-table" id="variants-table">
- <thead><tr><th>Name</th><th>SKU</th><th>Price override</th><th>Stock</th><th></th></tr></thead>
+ <thead><tr><th>Name</th><th>SKU</th><th>Different price</th><th>Stock</th><th></th></tr></thead>
  <tbody>
  <?php foreach ($variants as $variant): ?>
  <tr>
⋮ 36 unchanged lines ⋮
  <div class="sidebar-box">
  <label>Category</label>
  <select name="category_id" <?= $readOnlyMeta ? 'disabled' : '' ?>>
- <option value="">— none —</option>
+ <option value="">No category</option>
  <?php foreach ($categories as $category): ?>
  <option value="<?= (int) $category['id'] ?>" <?= (int) ($product['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>>
  <?= htmlspecialchars($category['name']) ?>
⋮ 1 unchanged line ⋮
  <?php endforeach; ?>
  </select>
  
- <label>Product type</label>
+ <label>What kind of product is it?</label>
  <select name="product_kind" <?= $readOnlyMeta ? 'disabled' : '' ?>>
- <option value="physical" <?= ($product['product_kind'] ?? 'physical') === 'physical' ? 'selected' : '' ?>>Physical good</option>
- <option value="digital" <?= ($product['product_kind'] ?? '') === 'digital' ? 'selected' : '' ?>>Digital download</option>
- <option value="saas_plan" <?= ($product['product_kind'] ?? '') === 'saas_plan' ? 'selected' : '' ?>>SaaS plan</option>
+ <option value="physical" <?= ($product['product_kind'] ?? 'physical') === 'physical' ? 'selected' : '' ?>>Something I ship</option>
+ <option value="digital" <?= ($product['product_kind'] ?? '') === 'digital' ? 'selected' : '' ?>>A file to download</option>
+ <option value="saas_plan" <?= ($product['product_kind'] ?? '') === 'saas_plan' ? 'selected' : '' ?>>A software plan (SaaS)</option>
  </select>
  
- <label>SKU</label>
+ <label>SKU (your own product code, optional)</label>
  <input type="text" name="sku" value="<?= htmlspecialchars((string) ($product['sku'] ?? '')) ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  </div>
  
⋮ 7 unchanged lines ⋮
  </datalist>
  
  <?php if ($pricingModel === 'subscription'): ?>
- <label>Billing interval</label>
+ <label>Customers pay</label>
  <select name="billing_interval" <?= $readOnlyMeta ? 'disabled' : '' ?>>
  <option value="month" <?= ($product['billing_interval'] ?? $storeSettings['subscription_interval']) === 'month' ? 'selected' : '' ?>>Monthly</option>
  <option value="year" <?= ($product['billing_interval'] ?? '') === 'year' ? 'selected' : '' ?>>Yearly</option>
- <option value="one_time" <?= ($product['billing_interval'] ?? '') === 'one_time' ? 'selected' : '' ?>>One-time (lifetime access)</option>
+ <option value="one_time" <?= ($product['billing_interval'] ?? '') === 'one_time' ? 'selected' : '' ?>>Once (keep forever)</option>
  </select>
  <label>Price</label>
  <div style="display:flex;gap:6px;">
⋮ 6 unchanged lines ⋮
  <input type="number" name="price_major" value="<?= (int) $priceMajor ?>" min="0" style="flex:2;" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  <input type="number" name="price_minor" value="<?= (int) $priceMinor ?>" min="0" style="flex:1;" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  </div>
- <label>Compare-at price (optional, shown crossed out)</label>
+ <label>Old price (optional, shown crossed out)</label>
  <div style="display:flex;gap:6px;">
  <input type="number" name="compare_price_major" value="<?= $comparePriceMajor !== null ? (int) $comparePriceMajor : '' ?>" min="0" style="flex:2;" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  <input type="number" name="compare_price_minor" value="<?= (int) $comparePriceMinor ?>" min="0" style="flex:1;" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  </div>
- <label><input type="checkbox" name="track_inventory" <?= (int) ($product['track_inventory'] ?? 0) === 1 ? 'checked' : '' ?> <?= $readOnlyMeta ? 'disabled' : '' ?>> Track inventory</label>
- <label>Stock quantity</label>
+ <label><input type="checkbox" name="track_inventory" <?= (int) ($product['track_inventory'] ?? 0) === 1 ? 'checked' : '' ?> <?= $readOnlyMeta ? 'disabled' : '' ?>> Keep track of stock</label>
+ <label>How many in stock</label>
  <input type="number" name="stock_quantity" value="<?= htmlspecialchars((string) ($product['stock_quantity'] ?? '0')) ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
- <label>Weight (grams, for shipping — optional)</label>
+ <label>Weight in grams (optional, for shipping)</label>
  <input type="number" name="weight_grams" value="<?= htmlspecialchars((string) ($product['weight_grams'] ?? '')) ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  <?php endif; ?>
  </div>
⋮ 4 unchanged lines ⋮
  $downloadLoginRequired = ($storeSettings['require_account_to_purchase'] ?? '0') === '1';
  ?>
  <div class="sidebar-box" id="product-files">
- <label><?= Icons::icon('download', 'icon icon-sm') ?>Downloadable files</label>
- <p class="product-files-hint">Customers get these files after paying for the product. If the price is 0, anyone can download them for free<?= $downloadLoginRequired ? ' after logging in (because "Require a customer account to purchase" is on)' : ' without an account' ?>.</p>
+ <label><?= Icons::icon('download', 'icon icon-sm') ?>Files to download</label>
+ <p class="product-files-hint">Customers get these files once they pay. If the price is 0, anyone can grab them for free<?= $downloadLoginRequired ? ' after logging in' : ', no account needed' ?>.</p>
  <?php if (!empty($productFiles) && DigitalFiles::isFree($product, $variants)): ?>
- <div class="dash-flash dash-flash-error" style="margin:0 0 10px;"><?= Icons::icon('info', 'icon icon-sm') ?>This product's price is 0, so these files are FREE to download<?= $downloadLoginRequired ? ' for any logged-in customer' : ' for anyone' ?>. Set a price to sell them.</div>
+ <div class="dash-flash dash-flash-error" style="margin:0 0 10px;"><?= Icons::icon('info', 'icon icon-sm') ?>The price is 0, so<?= $downloadLoginRequired ? ' any logged-in customer' : ' anyone' ?> can download these files for free. Set a price if you want to sell them.</div>
  <?php endif; ?>
  <?php if (!empty($productFiles)): ?>
  <ul class="product-files-list">
⋮ 1 unchanged line ⋮
  <li class="product-files-item">
  <div class="product-files-row">
  <span class="product-files-name"><?= htmlspecialchars($productFile['original_name']) ?><small><?= htmlspecialchars(DigitalFiles::formatSize((int) $productFile['size_bytes'])) ?></small></span>
- <button type="submit" form="product-file-delete-form" name="file_id" value="<?= (int) $productFile['id'] ?>" class="dash-btn-small dash-btn-danger" onclick="return confirm('Delete this file? Customers will no longer be able to download it.');"><?= Icons::icon('trash', 'icon icon-sm') ?></button>
+ <button type="submit" form="product-file-delete-form" name="file_id" value="<?= (int) $productFile['id'] ?>" class="dash-btn-small dash-btn-danger" onclick="return confirm('Delete this file? Customers won\'t be able to download it anymore.');"><?= Icons::icon('trash', 'icon icon-sm') ?></button>
  </div>
- <input type="text" name="button_label[<?= (int) $productFile['id'] ?>]" form="product-file-label-form" value="<?= htmlspecialchars((string) ($productFile['button_label'] ?? '')) ?>" maxlength="120" placeholder="Button text (optional), e.g. Download theme" aria-label="Button text for <?= htmlspecialchars($productFile['original_name']) ?>">
+ <input type="text" name="button_label[<?= (int) $productFile['id'] ?>]" form="product-file-label-form" value="<?= htmlspecialchars((string) ($productFile['button_label'] ?? '')) ?>" maxlength="120" placeholder="Button text (optional), like Download theme" aria-label="Button text for <?= htmlspecialchars($productFile['original_name']) ?>">
  </li>
  <?php endforeach; ?>
  </ul>
- <button type="submit" form="product-file-label-form" class="dash-btn-small"><?= Icons::icon('check', 'icon icon-sm') ?>Save button texts</button>
- <p class="product-files-hint">Leave the button text empty to show "Download" and the file name.</p>
+ <button type="submit" form="product-file-label-form" class="dash-btn-small"><?= Icons::icon('check', 'icon icon-sm') ?>Save button text</button>
+ <p class="product-files-hint">With no button text, we show "Download" and the file name.</p>
  <?php endif; ?>
  <input type="file" name="product_file" form="product-file-upload-form" required>
  <button type="submit" form="product-file-upload-form" class="dash-btn-small"><?= Icons::icon('upload', 'icon icon-sm') ?>Upload file</button>
- <p class="product-files-hint">Max <?= htmlspecialchars((string) ini_get('upload_max_filesize')) ?> per file on this server. Files are stored privately and only served through a protected download link.</p>
+ <p class="product-files-hint">Max <?= htmlspecialchars((string) ini_get('upload_max_filesize')) ?> per file. Files are kept private and can only be downloaded through a secure link.</p>
  </div>
  <?php endif; ?>
  
  <div class="sidebar-box">
- <label>Stripe price ID (optional override — leave blank to charge the price above directly)</label>
+ <label>Stripe price ID (optional, leave empty to charge the price above)</label>
  <input type="text" name="stripe_price_id" value="<?= htmlspecialchars((string) ($product['stripe_price_id'] ?? '')) ?>" placeholder="price_..." <?= $readOnlyMeta ? 'readonly' : '' ?>>
  <?php if ($storeCategory === 'saas' && $paymentProvider === 'polar'): ?>
  <label>Polar.sh product ID</label>
  <input type="text" name="polar_product_id" value="<?= htmlspecialchars((string) ($product['polar_product_id'] ?? '')) ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
- <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">Create this plan in your Polar.sh dashboard first, then paste its product ID here.</p>
+ <p style="font-size:12px;color:var(--muted);margin:6px 0 0;">Create the plan in Polar.sh first, then paste its product ID here.</p>
  <?php endif; ?>
  </div>
  
  <?php if (ProductTags::isEnabled()): ?>
  <div class="sidebar-box">
- <label>Tags (comma separated, up to <?= ProductTags::MAX_TAGS ?>)</label>
+ <label>Tags (separate with commas, up to <?= ProductTags::MAX_TAGS ?>)</label>
  <input type="text" name="tags" maxlength="700" value="<?= htmlspecialchars($product ? ProductTags::csvFor((int) $product['id']) : '') ?>" placeholder="new-arrival, bestseller" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  </div>
  <?php endif; ?>
⋮ 14 unchanged lines ⋮
  <?php endif; ?>
  
  <div class="sidebar-box">
- <label>Cover image URL</label>
+ <label>Main photo</label>
  <input type="text" name="cover_image_path" data-image-upload value="<?= htmlspecialchars($product['cover_image_path'] ?? '') ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  
- <label>SEO title</label>
+ <label>Title in Google (optional)</label>
  <input type="text" name="seo_title" value="<?= htmlspecialchars($product['seo_title'] ?? '') ?>" <?= $readOnlyMeta ? 'readonly' : '' ?>>
  
- <label>SEO description</label>
+ <label>Description in Google (optional)</label>
  <textarea name="seo_description" rows="2" <?= $readOnlyMeta ? 'readonly' : '' ?>><?= htmlspecialchars($product['seo_description'] ?? '') ?></textarea>
  </div>
  
  <?php if (!$readOnlyMeta): ?>
  <div class="sidebar-box">
- <label>Schedule for</label>
+ <label>Publish on</label>
  <input type="datetime-local" name="scheduled_at" value="<?= htmlspecialchars(!empty($product['scheduled_at']) ? date('Y-m-d\TH:i', strtotime((string) $product['scheduled_at'])) : '') ?>">
  
  <div class="publish-actions">
  <button type="submit" name="publish_action" value="save_draft" class="dash-btn"><?= Icons::icon('pages', 'icon icon-sm') ?>Save draft</button>
  <?php if (!Auth::canPublishDirectly()): ?>
- <button type="submit" name="publish_action" value="submit_for_review" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Submit for review</button>
+ <button type="submit" name="publish_action" value="submit_for_review" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Send for approval</button>
  <?php else: ?>
  <button type="submit" name="publish_action" value="publish_now" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Publish now</button>
  <button type="submit" name="publish_action" value="schedule" class="dash-btn"><?= Icons::icon('stats', 'icon icon-sm') ?>Schedule</button>
⋮ 1 unchanged line ⋮
  </div>
  </div>
  <?php else: ?>
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save proofreading</button>
+ <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save corrections</button>
  <?php endif; ?>
  
  <?php $productVersions = ($product && !$readOnlyMeta) ? ProductVersions::recent((int) $product['id']) : []; ?>
  <?php if (!empty($productVersions)): ?>
  <div class="sidebar-box">
- <label>Versions</label>
+ <label>Earlier versions</label>
  <ul style="list-style:none;padding:0;margin:0;display:grid;gap:10px;font-size:13px;">
  <?php foreach ($productVersions as $version): ?>
  <li style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
  <span><?= htmlspecialchars($version['created_at']) ?><br><span style="color:var(--muted);"><?= htmlspecialchars((string) $version['saved_by_name']) ?> &middot; <?= htmlspecialchars($version['note']) ?></span></span>
- <button type="submit" form="version-restore-form" name="version_id" value="<?= (int) $version['id'] ?>" class="dash-btn-small" onclick="return confirm('Restore this version? The current text is saved as a version first.');"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
+ <button type="submit" form="version-restore-form" name="version_id" value="<?= (int) $version['id'] ?>" class="dash-btn-small" onclick="return confirm('Go back to this version? Don\'t worry, we\'ll keep the current one too.');"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
  </li>
  <?php endforeach; ?>
  </ul>
⋮ 1 unchanged line ⋮
  <?php endif; ?>
  
  <?php if ($product && $product['status'] === 'rejected' && !empty($product['rejection_reason'])): ?>
- <div class="dash-flash dash-flash-error">Rejected: <?= htmlspecialchars($product['rejection_reason']) ?></div>
+ <div class="dash-flash dash-flash-error">Sent back with a note: <?= htmlspecialchars($product['rejection_reason']) ?></div>
  <?php endif; ?>
  
  </div>
⋮ 29 unchanged lines ⋮
dashboard-views/products.php Modified +11 -11
⋮ 13 unchanged lines ⋮
  $isAdmin = Auth::isAdministrative();
  
  $statement = $isAdmin
-     ? $db->query('SELECT p.*, t.display_name, c.name AS category_name FROM products p JOIN team_accounts t ON t.id = p.created_by LEFT JOIN categories c ON c.id = p.category_id WHERE p.deleted_at IS NULL ORDER BY p.updated_at DESC')
-     : $db->prepare('SELECT p.*, t.display_name, c.name AS category_name FROM products p JOIN team_accounts t ON t.id = p.created_by LEFT JOIN categories c ON c.id = p.category_id WHERE p.created_by = :id AND p.deleted_at IS NULL ORDER BY p.updated_at DESC');
+     ? $db->query('SELECT p.*, t.display_name, c.name AS category_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by LEFT JOIN categories c ON c.id = p.category_id WHERE p.deleted_at IS NULL ORDER BY p.updated_at DESC')
+     : $db->prepare('SELECT p.*, t.display_name, c.name AS category_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by LEFT JOIN categories c ON c.id = p.category_id WHERE p.created_by = :id AND p.deleted_at IS NULL ORDER BY p.updated_at DESC');
  
  if (!$isAdmin) {
      $statement->execute(['id' => $currentUser['id']]);
⋮ 32 unchanged lines ⋮
  <?php foreach ($products as $product): ?>
  <tr>
  <td><a href="dashboard.php?view=product-edit&id=<?= (int) $product['id'] ?>"><?= htmlspecialchars($product['title']) ?></a></td>
- <td><?= htmlspecialchars($product['category_name'] ?? '—') ?></td>
+ <td><?= htmlspecialchars($product['category_name'] ?? 'No category') ?></td>
  <td>
  <?php if (!empty($product['price_cents'])): ?>
  <?= htmlspecialchars(Currency::format((int) $product['price_cents'], (string) ($product['currency'] ?: $storeCurrency))) ?><?= $product['billing_interval'] && $product['billing_interval'] !== 'one_time' ? '/' . htmlspecialchars((string) $product['billing_interval']) : '' ?>
  <?php else: ?>
- —
+ Free
  <?php endif; ?>
  </td>
- <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars($product['status']) ?></span></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $product['status'])) ?></span></td>
  <td><?= htmlspecialchars($product['updated_at']) ?></td>
  <td class="dash-table-actions">
  
⋮ 5 unchanged lines ⋮
  <input type="hidden" name="decision" value="approve">
  <button type="submit" class="dash-btn-small dash-btn-success"><?= Icons::icon('check', 'icon icon-sm') ?>Approve</button>
  </form>
- <button type="button" class="dash-btn-small dash-btn-danger js-reject-btn" data-product-id="<?= (int) $product['id'] ?>"><?= Icons::icon('x', 'icon icon-sm') ?>Reject</button>
+ <button type="button" class="dash-btn-small dash-btn-danger js-reject-btn" data-product-id="<?= (int) $product['id'] ?>"><?= Icons::icon('x', 'icon icon-sm') ?>Send back</button>
  <?php endif; ?>
  
- <form method="post" style="display:inline;" onsubmit="return confirm('Move this product to the trash?');">
+ <form method="post" style="display:inline;" onsubmit="return confirm('Move this product to the trash? You can bring it back later.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="delete_product">
  <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
⋮ 2 unchanged lines ⋮
  <?php if ($product['status'] === 'published' && !empty($product['slug'])): ?>
  <a href="<?= htmlspecialchars(SiteFront::productUrl($product['slug'])) ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>View</a>
  <?php else: ?>
- <a href="product.php?preview=<?= (int) $product['id'] ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>View in preview mode</a>
+ <a href="product.php?preview=<?= (int) $product['id'] ?>" class="dash-btn-small" target="_blank" rel="noopener noreferrer"><?= Icons::icon('eye', 'icon icon-sm') ?>Preview</a>
  <?php endif; ?>
  </td>
  </tr>
⋮ 6 unchanged lines ⋮
  <input type="hidden" name="action" value="moderate_product">
  <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
  <input type="hidden" name="decision" value="reject">
- <input type="text" name="rejection_reason" placeholder="Rejection reason" class="dash-input-inline">
- <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('check', 'icon icon-sm') ?>Confirm reject</button>
+ <input type="text" name="rejection_reason" placeholder="What should be fixed?" class="dash-input-inline">
+ <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('check', 'icon icon-sm') ?>Send back</button>
  </form>
  </td>
  </tr>
⋮ 1 unchanged line ⋮
  
  <?php endforeach; ?>
  <?php if (empty($products)): ?>
- <tr><td colspan="6">No products found.</td></tr>
+ <tr><td colspan="6">No products yet. Click "New product" to add your first one.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 1 unchanged line ⋮
dashboard-views/reviews.php Modified +4 -4
⋮ 7 unchanged lines ⋮
  }
  
  if (!Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER)) {
-     echo '<p>You do not have permission to moderate reviews.</p>';
+     echo '<p>You don\'t have access to reviews.</p>';
      return;
  }
  
⋮ 26 unchanged lines ⋮
  </div>
  
  <div class="settings-tabs">
- <a href="dashboard.php?view=reviews&status=flagged" class="<?= $statusFilter === 'flagged' ? 'active' : '' ?>">Flagged</a>
+ <a href="dashboard.php?view=reviews&status=flagged" class="<?= $statusFilter === 'flagged' ? 'active' : '' ?>">Reported</a>
  <a href="dashboard.php?view=reviews&status=visible" class="<?= $statusFilter === 'visible' ? 'active' : '' ?>">Visible</a>
  <a href="dashboard.php?view=reviews&status=hidden" class="<?= $statusFilter === 'hidden' ? 'active' : '' ?>">Hidden</a>
  </div>
⋮ 15 unchanged lines ⋮
  <td><?= htmlspecialchars($productTitles[(int) $review['product_id']] ?? ('#' . (int) $review['product_id'])) ?></td>
  <td><?= htmlspecialchars((string) ($review['display_name'] ?? $review['email'] ?? 'Anonymous')) ?></td>
  <td><?= $review['rating'] ? str_repeat('�
- ', (int) $review['rating']) . str_repeat('☆', 5 - (int) $review['rating']) : '—' ?></td>
+ ', (int) $review['rating']) . str_repeat('☆', 5 - (int) $review['rating']) : 'No rating' ?></td>
  <td><?= htmlspecialchars(mb_strimwidth((string) $review['body'], 0, 140, '…')) ?></td>
  <td><?= htmlspecialchars($review['created_at']) ?></td>
  <td class="dash-table-actions">
⋮ 19 unchanged lines ⋮
  </tr>
  <?php endforeach; ?>
  <?php if (empty($reviews)): ?>
- <tr><td colspan="6">No reviews here.</td></tr>
+ <tr><td colspan="6">Nothing here right now.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 1 unchanged line ⋮
dashboard-views/security.php Modified +15 -15
⋮ 17 unchanged lines ⋮
  
  <?php if (is_array($newCodes) && !empty($newCodes)): ?>
  <div class="sidebar-box" style="max-width:640px;margin-bottom:20px;">
- <h2 class="dash-subtitle">Recovery codes</h2>
- <p>Save these codes in a safe place. Each one works once if you lose access to your authenticator app. They will not be shown again.</p>
+ <h2 class="dash-subtitle">Backup codes</h2>
+ <p>Keep these somewhere safe. If you lose your phone, each code gets you in once. You won't see them again.</p>
  <pre style="font-size:16px;line-height:1.8;"><?= htmlspecialchars(implode("\n", $newCodes)) ?></pre>
  </div>
  <?php endif; ?>
  
  <div class="sidebar-box" style="max-width:640px;">
- <h2 class="dash-subtitle">Two-factor authentication</h2>
+ <h2 class="dash-subtitle">Two-step login</h2>
  
  <?php if ($twoFactorEnabled): ?>
- <p>Status: <span class="status-pill status-published">Enabled</span> &middot; recovery codes left: <?= TwoFactor::remainingRecoveryCodes($currentUser) ?></p>
+ <p>Status: <span class="status-pill status-published">On</span> &middot; backup codes left: <?= TwoFactor::remainingRecoveryCodes($currentUser) ?></p>
  
  <form method="post" style="margin-top:16px;">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="totp_regenerate">
  <label>Password</label>
  <input type="password" name="password" required autocomplete="current-password">
- <label>Authentication code or a recovery code</label>
+ <label>Code from your app, or a backup code</label>
  <input type="text" name="code" required autocomplete="one-time-code" maxlength="16" spellcheck="false">
- <button type="submit" class="dash-btn" style="margin-top:12px;"><?= Icons::icon('refresh', 'icon icon-sm') ?>Generate new recovery codes</button>
+ <button type="submit" class="dash-btn" style="margin-top:12px;"><?= Icons::icon('refresh', 'icon icon-sm') ?>Get new backup codes</button>
  </form>
  
- <form method="post" style="margin-top:24px;" onsubmit="return confirm('Turn off two-factor authentication?');">
+ <form method="post" style="margin-top:24px;" onsubmit="return confirm('Turn off two-step login? Your account will only be protected by your password.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="totp_disable">
  <label>Password</label>
  <input type="password" name="password" required autocomplete="current-password">
- <label>Authentication code or a recovery code</label>
+ <label>Code from your app, or a backup code</label>
  <input type="text" name="code" required autocomplete="one-time-code" maxlength="16" spellcheck="false">
- <button type="submit" class="dash-btn dash-btn-danger" style="margin-top:12px;"><?= Icons::icon('x', 'icon icon-sm') ?>Turn off two-factor authentication</button>
+ <button type="submit" class="dash-btn dash-btn-danger" style="margin-top:12px;"><?= Icons::icon('x', 'icon icon-sm') ?>Turn off two-step login</button>
  </form>
  
  <?php elseif ($setupSecret !== ''): ?>
- <p>1. In your authenticator app (Google Authenticator, Authy, 1Password, Aegis…) scan this QR code. The code is drawn in your browser and the key never leaves this page.</p>
+ <p>1. Open an authenticator app (Google Authenticator, Authy, 1Password, Aegis and so on) and scan this QR code.</p>
  <div id="totp-qr" data-uri="<?= htmlspecialchars(Totp::uri($setupSecret, (string) $currentUser['username'], $issuer), ENT_QUOTES) ?>" style="width:220px;max-width:100%;margin:12px 0;border-radius:8px;overflow:hidden;background:#fff;"></div>
- <p style="font-size:13px;color:var(--muted);">Cannot scan it? Add the account manually (time based) with this key:</p>
+ <p style="font-size:13px;color:var(--muted);">Can't scan it? Type this key into the app instead:</p>
  <p style="font-size:20px;letter-spacing:.08em;margin:8px 0;"><code><?= htmlspecialchars(Totp::formatSecret($setupSecret)) ?></code></p>
- <p style="font-size:12px;color:var(--muted);word-break:break-all;">Account: <?= htmlspecialchars((string) $currentUser['username']) ?> &middot; Issuer: <?= htmlspecialchars($issuer) ?></p>
+ <p style="font-size:12px;color:var(--muted);word-break:break-all;">Account: <?= htmlspecialchars((string) $currentUser['username']) ?> &middot; Store: <?= htmlspecialchars($issuer) ?></p>
  <?= Asset::js('assets/vendor/qrcode.js') ?>
  <script>
  (function () {
⋮ 6 unchanged lines ⋮
      box.innerHTML = code.createSvgTag({ cellSize: 4, margin: 16, scalable: true });
  })();
  </script>
- <p style="margin-top:14px;">2. Enter the 6-digit code shown by the app to finish.</p>
+ <p style="margin-top:14px;">2. Type the 6-digit code from the app to finish.</p>
  
  <form method="post">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="totp_confirm">
- <label>Authentication code</label>
+ <label>6-digit code</label>
  <input type="text" name="code" required inputmode="numeric" autocomplete="one-time-code" maxlength="8" pattern="[0-9 ]*" spellcheck="false">
  <div class="publish-actions">
  <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Turn on</button>
⋮ 8 unchanged lines ⋮
  
  <?php else: ?>
  <p>Status: <span class="status-pill status-draft">Off</span></p>
- <p style="margin:10px 0 14px;">Add a second step to your login: a 6-digit code from an authenticator app, in addition to your password.</p>
+ <p style="margin:10px 0 14px;">Make your account harder to break into. After your password, you'll also type a 6-digit code from your phone.</p>
  <form method="post">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="totp_begin">
⋮ 5 unchanged lines ⋮
dashboard-views/stats.php Modified +7 -7
⋮ 54 unchanged lines ⋮
  <h1 class="dash-title"><?= Icons::icon("stats", "icon icon-lg") ?>Statistics</h1>
  
  <div class="dash-cards">
- <div class="dash-card"><?= Icons::icon('eye', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalViews) ?></div><div class="dash-card-label">Total views</div></div>
- <div class="dash-card"><?= Icons::icon('cart', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalSales) ?></div><div class="dash-card-label">Total sales</div></div>
- <div class="dash-card"><?= Icons::icon('download', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalDownloads) ?></div><div class="dash-card-label">Total downloads</div></div>
+ <div class="dash-card"><?= Icons::icon('eye', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalViews) ?></div><div class="dash-card-label">Product views</div></div>
+ <div class="dash-card"><?= Icons::icon('cart', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalSales) ?></div><div class="dash-card-label">Items sold</div></div>
+ <div class="dash-card"><?= Icons::icon('download', 'icon icon-lg') ?><div class="dash-card-value"><?= number_format($totalDownloads) ?></div><div class="dash-card-label">Downloads</div></div>
  </div>
  
  <?php if ($isManager && !empty($revenueByDay)): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('dollar', 'icon icon-sm') ?>Revenue (last 30 days)</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('dollar', 'icon icon-sm') ?>Sales in the last 30 days</h2>
  <table class="dash-table">
  <thead><tr><th><?= Icons::icon('clock', 'icon icon-sm') ?>Day</th><th><?= Icons::icon('dollar', 'icon icon-sm') ?>Revenue</th></tr></thead>
  <tbody>
⋮ 7 unchanged lines ⋮
  </table>
  <?php endif; ?>
  
- <h2 class="dash-subtitle"><?= Icons::icon('star', 'icon icon-sm') ?>Top products</h2>
+ <h2 class="dash-subtitle"><?= Icons::icon('star', 'icon icon-sm') ?>Best sellers</h2>
  <table class="dash-table">
  <thead><tr><th><?= Icons::icon('heading', 'icon icon-sm') ?>Title</th><th><?= Icons::icon('eye', 'icon icon-sm') ?>Views</th><th><?= Icons::icon('cart', 'icon icon-sm') ?>Sales</th><th><?= Icons::icon('download', 'icon icon-sm') ?>Downloads</th><th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th></tr></thead>
  <tbody>
⋮ 3 unchanged lines ⋮
  <td><?= number_format((int) $product['views_count']) ?></td>
  <td><?= number_format((int) $product['sales_count']) ?></td>
  <td><?= number_format((int) ($product['downloads_count'] ?? 0)) ?></td>
- <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars($product['status']) ?></span></td>
+ <td><span class="status-pill status-<?= htmlspecialchars($product['status']) ?>"><?= htmlspecialchars(StatusLabel::get((string) $product['status'])) ?></span></td>
  </tr>
  <?php endforeach; ?>
  <?php if (empty($topProducts)): ?>
- <tr><td colspan="5">No data yet.</td></tr>
+ <tr><td colspan="5">Nothing to show yet. Check back after your first sales.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 1 unchanged line ⋮
dashboard-views/trash.php Modified +13 -14
⋮ 10 unchanged lines ⋮
  $isAdmin = Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER);
  
  $statement = $isAdmin
-     ? $db->query('SELECT p.id, p.title, p.trashed_status, p.deleted_at, t.display_name FROM products p JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NOT NULL ORDER BY p.deleted_at DESC')
-     : $db->prepare('SELECT p.id, p.title, p.trashed_status, p.deleted_at, t.display_name FROM products p JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NOT NULL AND p.created_by = :id ORDER BY p.deleted_at DESC');
+     ? $db->query('SELECT p.id, p.title, p.trashed_status, p.deleted_at, t.display_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NOT NULL ORDER BY p.deleted_at DESC')
+     : $db->prepare('SELECT p.id, p.title, p.trashed_status, p.deleted_at, t.display_name FROM products p LEFT JOIN team_accounts t ON t.id = p.created_by WHERE p.deleted_at IS NOT NULL AND p.created_by = :id ORDER BY p.deleted_at DESC');
  
  if (!$isAdmin) {
      $statement->execute(['id' => $currentUser['id']]);
⋮ 24 unchanged lines ⋮
          . '<input type="hidden" name="action" value="' . htmlspecialchars($action) . '">'
          . ($productId !== null ? '<input type="hidden" name="product_id" value="' . $productId . '">' : '')
          . '<h2 class="dash-dialog-title">' . Icons::icon('download', 'icon icon-sm') . htmlspecialchars($question) . '</h2>'
-         . '<p class="dash-dialog-text">This will permanently delete the product' . ($productId === null ? 's' : '') . '. '
-         . 'Attached downloadable files: ' . $fileCount . '.</p>'
-         . '<p class="dash-dialog-text"><strong>Do you also want to remove access to the files?</strong></p>'
+         . '<p class="dash-dialog-text">' . ($productId === null ? 'These products will be gone for good. ' : 'This product will be gone for good. ') . 'Files attached: ' . $fileCount . '.</p>'
+         . '<p class="dash-dialog-text"><strong>What should happen to the files?</strong></p>'
          . '<ul class="dash-dialog-list">'
-         . '<li><strong>Keep files</strong> — customers who already bought the product can still download them. The files stay under Files.</li>'
-         . '<li><strong>Delete files</strong> — the files are erased and every customer loses access. This cannot be undone.</li>'
+         . '<li><strong>Keep the files</strong> if people who already bought it should still be able to download. They stay under Files.</li>'
+         . '<li><strong>Delete the files</strong> to erase them for everyone, including past buyers. This can\'t be undone.</li>'
          . '</ul>'
          . '<div class="dash-dialog-actions">'
          . '<button type="button" class="dash-btn" onclick="this.closest(\'dialog\').close();">Cancel</button>'
-         . '<button type="submit" name="file_action" value="keep" class="dash-btn dash-btn-primary">Delete product, keep files</button>'
-         . '<button type="submit" name="file_action" value="delete" class="dash-btn dash-btn-danger">Delete product and files</button>'
+         . '<button type="submit" name="file_action" value="keep" class="dash-btn dash-btn-primary">Delete, keep the files</button>'
+         . '<button type="submit" name="file_action" value="delete" class="dash-btn dash-btn-danger">Delete everything</button>'
          . '</div>'
          . '</form>'
          . '</dialog>';
⋮ 8 unchanged lines ⋮
  <button type="button" class="dash-btn dash-btn-danger" onclick="document.getElementById('trash-dialog-all').showModal();"><?= Icons::icon('trash', 'icon icon-sm') ?>Empty trash</button>
  <?= $renderFileDialog('trash-dialog-all', 'empty_trash', null, 'Empty the trash?', array_sum($trashedFileCounts)) ?>
  <?php else: ?>
- <form method="post" style="display:inline;" onsubmit="return confirm('Permanently delete everything in the trash? This cannot be undone.');">
+ <form method="post" style="display:inline;" onsubmit="return confirm('Empty the trash? Everything in it will be gone for good.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="empty_trash">
  <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Empty trash</button>
⋮ 6 unchanged lines ⋮
  <thead>
  <tr>
  <th><?= Icons::icon('heading', 'icon icon-sm') ?>Title</th>
- <th><?= Icons::icon('user', 'icon icon-sm') ?>Created by</th>
+ <th><?= Icons::icon('user', 'icon icon-sm') ?>Added by</th>
  <th><?= Icons::icon('flag', 'icon icon-sm') ?>Was</th>
  <th><?= Icons::icon('clock', 'icon icon-sm') ?>Deleted</th>
  <th></th>
⋮ 3 unchanged lines ⋮
  <?php foreach ($trashed as $product): ?>
  <tr>
  <td><?= htmlspecialchars($product['title']) ?><?php if (($trashedFileCounts[(int) $product['id']] ?? 0) > 0): ?> <span class="status-pill status-scheduled"><?= (int) $trashedFileCounts[(int) $product['id']] ?> file<?= $trashedFileCounts[(int) $product['id']] === 1 ? '' : 's' ?></span><?php endif; ?></td>
- <td><?= htmlspecialchars($product['display_name']) ?></td>
+ <td><?= htmlspecialchars((string) ($product['display_name'] ?? 'System')) ?></td>
  <td><?= htmlspecialchars((string) ($product['trashed_status'] ?? 'draft')) ?></td>
  <td><?= htmlspecialchars($product['deleted_at']) ?></td>
  <td class="dash-table-actions">
⋮ 8 unchanged lines ⋮
  <button type="button" class="dash-btn-small dash-btn-danger" onclick="document.getElementById('trash-dialog-<?= (int) $product['id'] ?>').showModal();"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete forever</button>
  <?= $renderFileDialog('trash-dialog-' . (int) $product['id'], 'purge_product', (int) $product['id'], 'Delete "' . $product['title'] . '" forever?', $productFileCount) ?>
  <?php else: ?>
- <form method="post" style="display:inline;" onsubmit="return confirm('Permanently delete this product? This cannot be undone.');">
+ <form method="post" style="display:inline;" onsubmit="return confirm('Delete this product for good? This can\'t be undone.');">
  <?= Csrf::field() ?>
  <input type="hidden" name="action" value="purge_product">
  <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
⋮ 4 unchanged lines ⋮
  </tr>
  <?php endforeach; ?>
  <?php if (empty($trashed)): ?>
- <tr><td colspan="5">The trash is empty.</td></tr>
+ <tr><td colspan="5">The trash is empty. Nice and tidy.</td></tr>
  <?php endif; ?>
  </tbody>
  </table>
⋮ 1 unchanged line ⋮
dashboard.php Modified +5 -5
⋮ 32 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (empty($_POST) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
-         $flashMessage = 'The upload is larger than this server allows (post_max_size = ' . ini_get('post_max_size') . ', upload_max_filesize = ' . ini_get('upload_max_filesize') . ').';
+         $flashMessage = 'That file is too big for your server. The limit is ' . ini_get('upload_max_filesize') . ', ask your host if you need more.';
          $flashType = 'error';
      } elseif (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = $_POST['action'] ?? '';
⋮ 164 unchanged lines ⋮
  <?php endif; ?>
  
  <?php if (isset($_GET['saved'])): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Saved successfully.</div>
+ <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Changes saved.</div>
  <?php endif; ?>
  <?php if (isset($_GET['deleted'])): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Deleted successfully.</div>
+ <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Deleted.</div>
  <?php endif; ?>
  <?php if (isset($_GET['moderated'])): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Moderation action applied.</div>
+ <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>Status updated.</div>
  <?php endif; ?>
  
  <?php
⋮ 48 unchanged lines ⋮
files.php Modified +22 -22
⋮ 34 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (empty($_POST) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
-         $flashMessage = 'The upload is larger than this server allows (post_max_size = ' . ini_get('post_max_size') . ', upload_max_filesize = ' . ini_get('upload_max_filesize') . ').';
+         $flashMessage = 'That file is too big for your server. The limit is ' . ini_get('upload_max_filesize') . ', ask your host if you need more.';
          $flashType = 'error';
      } elseif (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = (string) ($_POST['action'] ?? '');
⋮ 3 unchanged lines ⋮
          if ($action === 'upload_file') {
              $targetProduct = $findLiveProduct((int) ($_POST['product_id'] ?? 0));
              if ($targetProduct === null) {
-                 $flashMessage = 'Choose a product for the file.';
+                 $flashMessage = 'Pick which product this file belongs to.';
                  $flashType = 'error';
              } else {
                  [$stored, $flashMessage] = DigitalFiles::store((int) $targetProduct['id'], $_FILES['product_file'] ?? null);
⋮ 8 unchanged lines ⋮
          } elseif ($action === 'rename_file') {
              $newName = trim((string) ($_POST['name'] ?? ''));
              if ($newName === '') {
-                 $flashMessage = 'Enter a file name.';
+                 $flashMessage = 'Please give the file a name.';
                  $flashType = 'error';
              } else {
                  DigitalFiles::rename($fileId, $newName);
                  DigitalFiles::setButtonLabel($fileId, (string) ($_POST['button_label'] ?? ''));
                  ActivityLog::record('product.file_rename', 'product', (int) $file['product_id'], mb_substr($newName, 0, 120));
-                 $flashMessage = 'File updated.';
+                 $flashMessage = 'Changes saved.';
              }
          } elseif ($action === 'move_file') {
              $targetProduct = $findLiveProduct((int) ($_POST['product_id'] ?? 0));
              if ($targetProduct === null) {
-                 $flashMessage = 'Choose a product to move the file to.';
+                 $flashMessage = 'Pick the product to move the file to.';
                  $flashType = 'error';
              } elseif ((int) $targetProduct['id'] === (int) $file['product_id'] && ($file['detached_at'] ?? null) === null) {
-                 $flashMessage = 'The file is already attached to that product.';
+                 $flashMessage = 'The file already belongs to that product.';
                  $flashType = 'error';
              } else {
                  DigitalFiles::move($fileId, (int) $targetProduct['id']);
⋮ 3 unchanged lines ⋮
          } elseif ($action === 'delete_file') {
              DigitalFiles::delete($fileId);
              ActivityLog::record('product.file_delete', 'product', (int) $file['product_id'], mb_substr((string) $file['original_name'], 0, 120));
-             $flashMessage = 'File deleted. Customers can no longer download it.';
+             $flashMessage = 'File deleted. Nobody can download it anymore.';
          }
  
          if ($flashType === 'success' && $flashMessage !== null) {
⋮ 37 unchanged lines ⋮
      <div class="dash-card">
          <?= Icons::icon('layers') ?>
          <div class="dash-card-value"><?= count($files) ?></div>
-         <div class="dash-card-label"><?= $filter === 'all' && $search === '' ? 'Files' : 'Files shown' ?></div>
+         <div class="dash-card-label"><?= $filter === 'all' && $search === '' ? 'Files' : 'Matching files' ?></div>
      </div>
      <div class="dash-card">
          <?= Icons::icon('download') ?>
⋮ 7 unchanged lines ⋮
      </div>
      <div class="dash-card">
          <?= Icons::icon('shield') ?>
-         <div class="dash-card-value" style="font-size:14px;"><?= $storage['path'] === null ? 'Not writable' : ($storage['outside_web_root'] ? 'Outside web root' : 'private-downloads/') ?></div>
+         <div class="dash-card-value" style="font-size:14px;"><?= $storage['path'] === null ? 'Not set up' : ($storage['outside_web_root'] ? 'Safe, outside your site' : 'Private folder') ?></div>
          <div class="dash-card-label">Storage<?= $storage['free_bytes'] !== null ? ' · ' . htmlspecialchars(DigitalFiles::formatSize((int) $storage['free_bytes'])) . ' free' : '' ?></div>
      </div>
  </div>
  
  <?php if ($storage['path'] === null): ?>
-     <div class="dash-flash dash-flash-error"><?= Icons::icon('info', 'icon icon-sm') ?>No writable folder for downloads. Create "stocketbase-downloads" next to the site folder (recommended) or "private-downloads" inside it, and make it writable by PHP.</div>
+     <div class="dash-flash dash-flash-error"><?= Icons::icon('info', 'icon icon-sm') ?>There's nowhere to keep files yet. Create a folder called "stocketbase-downloads" next to your site folder (best) or "private-downloads" inside it, and let the site write to it. Your host can help with this.</div>
  <?php elseif (!$storage['outside_web_root']): ?>
-     <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Files are stored in private-downloads/ inside the site folder, protected by .htaccess. On Nginx, block web access to this folder, or create "stocketbase-downloads" next to the site folder so files are kept outside the web root.</p>
+     <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Files are kept in a private folder inside your site. On most hosts that's safe as it is. If your server uses Nginx, create a "stocketbase-downloads" folder next to your site folder instead so nobody can open the files directly.</p>
  <?php endif; ?>
  
  <h2 class="dash-subtitle"><?= Icons::icon('upload', 'icon icon-sm') ?>Upload a file</h2>
⋮ 3 unchanged lines ⋮
      <div>
          <label>Product</label>
          <select name="product_id" required>
-             <option value="">— choose a product —</option>
+             <option value="">Pick a product</option>
              <?php foreach ($liveProducts as $liveProduct): ?>
                  <option value="<?= (int) $liveProduct['id'] ?>"><?= htmlspecialchars($liveProduct['title']) ?></option>
              <?php endforeach; ?>
⋮ 17 unchanged lines ⋮
          <select name="filter" onchange="this.form.submit()">
              <option value="all" <?= $filter === 'all' ? 'selected' : '' ?>>All files</option>
              <option value="attached" <?= $filter === 'attached' ? 'selected' : '' ?>>Attached to a product</option>
-             <option value="detached" <?= $filter === 'detached' ? 'selected' : '' ?>>From deleted products</option>
+             <option value="detached" <?= $filter === 'detached' ? 'selected' : '' ?>>Left over from deleted products</option>
          </select>
      </div>
      <button type="submit" class="dash-btn"><?= Icons::icon('search', 'icon icon-sm') ?>Filter</button>
⋮ 4 unchanged lines ⋮
          <tr>
              <th>File</th>
              <th>Product</th>
-             <th>Access</th>
+             <th>Who can download</th>
              <th>Downloads</th>
              <th>Uploaded</th>
              <th></th>
⋮ 17 unchanged lines ⋮
                          <input type="hidden" name="action" value="rename_file">
                          <input type="hidden" name="file_id" value="<?= (int) $file['id'] ?>">
                          <input type="text" name="name" value="<?= htmlspecialchars($file['original_name']) ?>" maxlength="180" aria-label="File name" title="File name">
-                         <input type="text" name="button_label" value="<?= htmlspecialchars((string) ($file['button_label'] ?? '')) ?>" maxlength="120" placeholder="Button text (optional)" aria-label="Button text" title="Button text shown to customers instead of the file name">
+                         <input type="text" name="button_label" value="<?= htmlspecialchars((string) ($file['button_label'] ?? '')) ?>" maxlength="120" placeholder="Button text (optional)" aria-label="Button text" title="Customers see this on the button instead of the file name">
                          <button type="submit" class="dash-btn-small"><?= Icons::icon('edit', 'icon icon-sm') ?>Save</button>
                      </form>
                  </td>
⋮ 10 unchanged lines ⋮
                      <?php if ($isFreeFile): ?>
                          <span class="status-pill status-published">Free</span>
                      <?php elseif ($isDetached): ?>
-                         <span class="status-pill status-scheduled">Past buyers only</span>
+                         <span class="status-pill status-scheduled">Only past buyers</span>
                      <?php else: ?>
-                         <span class="status-pill status-paid">Buyers</span>
+                         <span class="status-pill status-paid">Buyers only</span>
                      <?php endif; ?>
                  </td>
                  <td>
                      <?= number_format((int) ($file['download_count'] ?? 0)) ?>
                      <?php if ($counts !== null): ?>
-                         <br><small style="color:var(--muted);"><?= (int) $counts['links'] ?> email link<?= (int) $counts['links'] === 1 ? '' : 's' ?> issued</small>
+                         <br><small style="color:var(--muted);"><?= (int) $counts['links'] ?> email link<?= (int) $counts['links'] === 1 ? '' : 's' ?> sent</small>
                      <?php endif; ?>
                  </td>
                  <td><?= htmlspecialchars(substr((string) $file['created_at'], 0, 16)) ?></td>
                  <td class="dash-table-actions">
                      <a href="files.php?download=<?= (int) $file['id'] ?>" class="dash-btn-small"><?= Icons::icon('download', 'icon icon-sm') ?>Download</a>
-                     <form method="post" class="files-inline-form" onsubmit="return confirm('Move this file? Customers who bought the current product will lose access to it, and buyers of the new product will get access.');">
+                     <form method="post" class="files-inline-form" onsubmit="return confirm('Move this file? People who bought the current product lose access, and buyers of the new one get it.');">
                          <?= Csrf::field() ?>
                          <input type="hidden" name="action" value="move_file">
                          <input type="hidden" name="file_id" value="<?= (int) $file['id'] ?>">
⋮ 5 unchanged lines ⋮
                          </select>
                          <button type="submit" class="dash-btn-small">Move</button>
                      </form>
-                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this file permanently? Every customer loses access to it. This cannot be undone.');">
+                     <form method="post" style="display:inline;" onsubmit="return confirm('Delete this file for good? Nobody will be able to download it anymore.');">
                          <?= Csrf::field() ?>
                          <input type="hidden" name="action" value="delete_file">
                          <input type="hidden" name="file_id" value="<?= (int) $file['id'] ?>">
⋮ 3 unchanged lines ⋮
              </tr>
          <?php endforeach; ?>
          <?php if (empty($files)): ?>
-             <tr><td colspan="6"><?= $filter === 'all' && $search === '' ? 'No files yet. Upload one above or from a product\'s edit page.' : 'No files match your filter.' ?></td></tr>
+             <tr><td colspan="6"><?= $filter === 'all' && $search === '' ? 'No files yet. Upload one above, or from a product\'s page.' : 'Nothing matches your search.' ?></td></tr>
          <?php endif; ?>
      </tbody>
  </table>
⋮ 3 unchanged lines ⋮
includes/ai.php Modified

Not shown (binary file or too large to diff).

includes/antibot.php Modified +2 -1
⋮ 62 unchanged lines ⋮
          $width = 200;
          $height = 70;
          $length = strlen($token);
-         $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $width . '" height="' . $height . '" viewBox="0 0 ' . $width . ' ' . $height . '" role="img" aria-label="Security verification code">';
+         $imageLabel = class_exists('Language') ? Language::get('antibot_image_label', 'Security code') : 'Security code';
+         $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $width . '" height="' . $height . '" viewBox="0 0 ' . $width . ' ' . $height . '" role="img" aria-label="' . htmlspecialchars($imageLabel, ENT_QUOTES) . '">';
          $svg .= '<defs>';
          $svg .= '<filter id="antibot-noise">';
          $svg .= '<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>';
⋮ 93 unchanged lines ⋮
includes/block-editor.php Modified +123 -3
⋮ 21 unchanged lines ⋮
          'embed',
      ];
  
-     private const SINGLE_USE_TYPES = ['contact_form'];
+     private const SINGLE_USE_TYPES = ['contact_form', 'home_featured', 'home_products'];
  
+     public const HOME_SYSTEM_TYPES = ['home_featured', 'home_products'];
+ 
      private static bool $interactive = true;
  
-     public static function sanitize(string $rawJson, bool $allowContactForm = false): string
+     public static function sanitize(string $rawJson, bool $allowContactForm = false, bool $homepage = false): string
      {
          $decoded = json_decode($rawJson, true);
  
          if (!is_array($decoded) || !isset($decoded['blocks']) || !is_array($decoded['blocks'])) {
-             return json_encode(['blocks' => []]);
+             if (!$homepage) {
+                 return json_encode(['blocks' => []]);
+             }
+             $decoded = ['blocks' => []];
          }
  
          $cleanBlocks = [];
          $usedSingleTypes = [];
          $allowedTypes = $allowContactForm ? array_merge(self::ALLOWED_TYPES, ['contact_form']) : self::ALLOWED_TYPES;
+         if ($homepage) {
+             $allowedTypes = array_merge($allowedTypes, ['hero'], self::HOME_SYSTEM_TYPES);
+         }
  
          foreach ($decoded['blocks'] as $block) {
              if (!is_array($block) || !isset($block['type']) || !in_array($block['type'], $allowedTypes, true)) {
⋮ 16 unchanged lines ⋮
              ];
          }
  
+         if ($homepage) {
+             $cleanBlocks = self::withSystemBlocks($cleanBlocks);
+         }
+ 
          return json_encode(['blocks' => $cleanBlocks], JSON_UNESCAPED_UNICODE);
      }
  
+     private static function withSystemBlocks(array $blocks): array
+     {
+         $present = [];
+         foreach ($blocks as $block) {
+             $present[(string) ($block['type'] ?? '')] = true;
+         }
+ 
+         foreach (self::HOME_SYSTEM_TYPES as $type) {
+             if (!isset($present[$type])) {
+                 $blocks[] = ['id' => 'sys_' . $type, 'type' => $type, 'data' => []];
+             }
+         }
+ 
+         return $blocks;
+     }
+ 
+     public static function defaultHomepageBlocks(bool $withHero): string
+     {
+         $blocks = [];
+         if ($withHero) {
+             $blocks[] = ['id' => 'home_hero', 'type' => 'hero', 'data' => ['eyebrow' => '', 'title' => '', 'subtitle' => '', 'show_eyebrow' => true]];
+         }
+         foreach (self::HOME_SYSTEM_TYPES as $type) {
+             $blocks[] = ['id' => 'sys_' . $type, 'type' => $type, 'data' => []];
+         }
+ 
+         return json_encode(['blocks' => $blocks], JSON_UNESCAPED_UNICODE);
+     }
+ 
+     public static function renderHomepage(string $blocksJson, array $settings, array $systemRenderers): string
+     {
+         $decoded = json_decode($blocksJson, true);
+         $blocks = is_array($decoded) && is_array($decoded['blocks'] ?? null) ? $decoded['blocks'] : [];
+         $blocks = self::withSystemBlocks(array_values(array_filter($blocks, 'is_array')));
+ 
+         self::$interactive = true;
+         $html = '';
+         $buffer = '';
+         $rendered = [];
+ 
+         foreach ($blocks as $block) {
+             $type = (string) ($block['type'] ?? '');
+             $data = is_array($block['data'] ?? null) ? $block['data'] : [];
+ 
+             if (in_array($type, self::SINGLE_USE_TYPES, true)) {
+                 if (isset($rendered[$type])) {
+                     continue;
+                 }
+                 $rendered[$type] = true;
+             }
+ 
+             if ($type === 'contact_form') {
+                 continue;
+             }
+ 
+             if ($type !== 'hero' && !in_array($type, self::HOME_SYSTEM_TYPES, true)) {
+                 $buffer .= self::renderBlock(['type' => $type, 'data' => $data]);
+                 continue;
+             }
+ 
+             if ($buffer !== '') {
+                 $html .= '<div class="article-content home-content-block">' . $buffer . '</div>';
+                 $buffer = '';
+             }
+ 
+             if ($type === 'hero') {
+                 $html .= self::renderHero($data, $settings);
+                 continue;
+             }
+ 
+             $renderer = $systemRenderers[$type] ?? null;
+             $html .= is_callable($renderer) ? (string) $renderer() : '';
+         }
+ 
+         if ($buffer !== '') {
+             $html .= '<div class="article-content home-content-block">' . $buffer . '</div>';
+         }
+ 
+         return $html;
+     }
+ 
+     private static function renderHero(array $data, array $settings): string
+     {
+         $title = trim((string) ($data['title'] ?? ''));
+         $title = $title !== '' ? $title : trim((string) ($settings['site_name'] ?? ''));
+         $subtitle = trim((string) ($data['subtitle'] ?? ''));
+         $subtitle = $subtitle !== '' ? $subtitle : trim((string) ($settings['site_description'] ?? ''));
+         $eyebrow = trim((string) ($data['eyebrow'] ?? ''));
+         $eyebrow = $eyebrow !== '' ? $eyebrow : Language::get('nav_home', 'Home');
+         $showEyebrow = !array_key_exists('show_eyebrow', $data) || !empty($data['show_eyebrow']);
+ 
+         if ($title === '' && $subtitle === '') {
+             return '';
+         }
+ 
+         return '<section class="hero-section">'
+             . ($showEyebrow ? '<div class="hero-eyebrow">' . htmlspecialchars($eyebrow) . '</div>' : '')
+             . ($title !== '' ? '<h1 class="hero-title">' . htmlspecialchars($title) . '</h1>' : '')
+             . ($subtitle !== '' ? '<p class="hero-subtitle">' . htmlspecialchars($subtitle) . '</p>' : '')
+             . '</section>';
+     }
+ 
      private static function sanitizeBlockData(string $type, array $data): array
      {
          return match ($type) {
⋮ 39 unchanged lines ⋮
              'separator' => [],
              'contact_form' => [
                  'title' => mb_substr(trim(strip_tags((string) ($data['title'] ?? ''))), 0, 120),
+             ],
+             'hero' => [
+                 'eyebrow' => mb_substr(trim(strip_tags((string) ($data['eyebrow'] ?? ''))), 0, 80),
+                 'title' => mb_substr(trim(strip_tags((string) ($data['title'] ?? ''))), 0, 160),
+                 'subtitle' => mb_substr(trim(strip_tags((string) ($data['subtitle'] ?? ''))), 0, 400),
+                 'show_eyebrow' => !array_key_exists('show_eyebrow', $data) || !empty($data['show_eyebrow']),
              ],
              default => [],
          };
⋮ 333 unchanged lines ⋮
includes/dash-header.php Modified +5 -4
⋮ 14 unchanged lines ⋮
  $dashIsAdmin = Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER);
  $dashIsManager = Auth::hasRoleAtLeast(Auth::ROLE_STORE_MANAGER);
  require_once __DIR__ . '/avatar.php';
+ require_once __DIR__ . '/status-label.php';
  
  ?>
  <!DOCTYPE html>
⋮ 70 unchanged lines ⋮
      <div class="dash-mobile-topbar">
          <div class="dash-brand"><span class="dash-brand-dot"></span><?= htmlspecialchars($dashSiteName) ?></div>
          <div class="dash-mobile-topbar-right">
-             <button type="button" class="dash-theme-toggle" aria-label="Toggle theme">
+             <button type="button" class="dash-theme-toggle" aria-label="Switch between light and dark">
                  <?= Icons::icon('sun', 'icon icon-sm icon-sun') ?>
                  <?= Icons::icon('moon', 'icon icon-sm icon-moon') ?>
              </button>
⋮ 6 unchanged lines ⋮
  
              <div class="dash-brand-row">
                  <div class="dash-brand"><span class="dash-brand-dot"></span><?= htmlspecialchars($dashSiteName) ?></div>
-                 <button type="button" class="dash-theme-toggle" id="dash-theme-toggle-btn" aria-label="Toggle theme">
+                 <button type="button" class="dash-theme-toggle" id="dash-theme-toggle-btn" aria-label="Switch between light and dark">
                      <?= Icons::icon('sun', 'icon icon-sm icon-sun') ?>
                      <?= Icons::icon('moon', 'icon icon-sm icon-moon') ?>
                  </button>
⋮ 13 unchanged lines ⋮
                  <a href="dashboard.php?view=pages" class="<?= ($dashView ?? '') === 'pages' ? 'active' : '' ?>"><?= Icons::icon('pages') ?>Pages</a>
                  <a href="dashboard.php?view=stats" class="<?= ($dashView ?? '') === 'stats' ? 'active' : '' ?>"><?= Icons::icon('stats') ?>Statistics</a>
                  <a href="dashboard.php?view=security" class="<?= ($dashView ?? '') === 'security' ? 'active' : '' ?>"><?= Icons::icon('lock') ?>Security</a>
-                 <a href="interface.php" class="<?= ($dashActivePage ?? '') === 'interface' ? 'active' : '' ?>"><?= Icons::icon('sliders') ?>Interface</a>
+                 <a href="interface.php" class="<?= ($dashActivePage ?? '') === 'interface' ? 'active' : '' ?>"><?= Icons::icon('sliders') ?>Appearance</a>
                  <?php if ($dashIsAdmin): ?>
                      <div class="dash-nav-divider"></div>
-                     <a href="admin.php" class="<?= ($dashActivePage ?? '') === 'admin' ? 'active' : '' ?>"><?= Icons::icon('team') ?>Team accounts</a>
+                     <a href="admin.php" class="<?= ($dashActivePage ?? '') === 'admin' ? 'active' : '' ?>"><?= Icons::icon('team') ?>Team</a>
                      <a href="settings.php" class="<?= ($dashActivePage ?? '') === 'settings' ? 'active' : '' ?>"><?= Icons::icon('settings') ?>Settings</a>
                      <a href="languages.php" class="<?= ($dashActivePage ?? '') === 'languages' ? 'active' : '' ?>"><?= Icons::icon('translate') ?>Languages</a>
                      <a href="ads.php" class="<?= ($dashActivePage ?? '') === 'ads' ? 'active' : '' ?>"><?= Icons::icon('ads') ?>Ads</a>
⋮ 18 unchanged lines ⋮
includes/database.php Modified +51 -0
⋮ 86 unchanged lines ⋮
                      'deleted_at' => "ALTER TABLE products ADD COLUMN deleted_at DATETIME NULL",
                      'trashed_status' => "ALTER TABLE products ADD COLUMN trashed_status VARCHAR(20) NULL",
                  ],
+                 'pages' => [
+                     'is_home' => "ALTER TABLE pages ADD COLUMN is_home TINYINT(1) NOT NULL DEFAULT 0",
+                 ],
                  'categories' => [
                      'show_in_menu' => "ALTER TABLE categories ADD COLUMN show_in_menu TINYINT(1) NOT NULL DEFAULT 1 AFTER sort_order",
                  ],
⋮ 27 unchanged lines ⋮
                              error_log('StocketBase: could not auto-add column ' . $table . '.' . $column . ': ' . $e->getMessage());
                          }
                      }
+                 }
+             }
+ 
+             $authorColumns = ['products' => 'created_by', 'pages' => 'author_id', 'ads' => 'created_by', 'discounts' => 'created_by'];
+             $authorState = $pdo->prepare(
+                 "SELECT c.TABLE_NAME, c.IS_NULLABLE, k.CONSTRAINT_NAME, r.DELETE_RULE
+                  FROM information_schema.COLUMNS c
+                  LEFT JOIN information_schema.KEY_COLUMN_USAGE k
+                    ON k.TABLE_SCHEMA = c.TABLE_SCHEMA AND k.TABLE_NAME = c.TABLE_NAME AND k.COLUMN_NAME = c.COLUMN_NAME AND k.REFERENCED_TABLE_NAME = 'team_accounts'
+                  LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS r
+                    ON r.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND r.TABLE_NAME = k.TABLE_NAME
+                  WHERE c.TABLE_SCHEMA = :db
+                    AND ((c.TABLE_NAME = 'products' AND c.COLUMN_NAME = 'created_by')
+                      OR (c.TABLE_NAME = 'pages' AND c.COLUMN_NAME = 'author_id')
+                      OR (c.TABLE_NAME = 'ads' AND c.COLUMN_NAME = 'created_by')
+                      OR (c.TABLE_NAME = 'discounts' AND c.COLUMN_NAME = 'created_by'))"
+             );
+             $authorState->execute(['db' => $dbName]);
+             foreach ($authorState->fetchAll(PDO::FETCH_ASSOC) as $state) {
+                 $table = (string) $state['TABLE_NAME'];
+                 $column = $authorColumns[$table] ?? null;
+                 if ($column === null) {
+                     continue;
+                 }
+                 $constraint = (string) ($state['CONSTRAINT_NAME'] ?? '');
+                 $needsNullable = $state['IS_NULLABLE'] === 'NO';
+                 $needsRule = $constraint !== '' && strtoupper((string) $state['DELETE_RULE']) !== 'SET NULL';
+                 if (!$needsNullable && !$needsRule) {
+                     continue;
+                 }
+                 if (preg_match('/^[A-Za-z0-9_]+$/', $constraint) !== 1 && $constraint !== '') {
+                     continue;
+                 }
+                 try {
+                     if ($needsRule) {
+                         $pdo->exec('ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $constraint);
+                     }
+                     if ($needsNullable) {
+                         $pdo->exec('ALTER TABLE ' . $table . ' MODIFY ' . $column . ' INT UNSIGNED NULL');
+                     }
+                     if ($needsRule) {
+                         $pdo->exec('ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint . ' FOREIGN KEY (' . $column . ') REFERENCES team_accounts(id) ON DELETE SET NULL');
+                     }
+                     if ($table === 'pages') {
+                         $pdo->exec('UPDATE pages SET author_id = NULL WHERE is_home = 1');
+                     }
+                 } catch (PDOException $e) {
+                     error_log('StocketBase: could not update author key on ' . $table . ': ' . $e->getMessage());
                  }
              }
  
⋮ 135 unchanged lines ⋮
includes/language.php Modified +11 -20
⋮ 42 unchanged lines ⋮
  
      public const ADDED_DEFAULTS = [
          'language_switcher_label' => 'Language',
-         'article_toc_title' => 'Table of contents',
          'back_to_top' => 'Back to top',
          'reading_progress_left' => '{percent}% left',
          'breadcrumb_label' => 'Breadcrumb',
-         'article_written_by' => 'Written by',
-         'author_articles_count' => 'Articles: {count}',
-         'artist_articles_title' => 'Articles by {name}',
-         'artist_not_found' => 'Author not found.',
-         'artist_website' => 'Website',
-         'artist_no_articles' => 'No published articles yet.',
-         'article_tags_label' => 'Tags:',
+         'product_tags_label' => 'Tags:',
          'tag_page_title' => 'Tagged: {name}',
          'tag_not_found' => 'Tag not found.',
-         'tag_no_articles' => 'No published articles with this tag yet.',
-         'read_aloud_title' => 'Listen to this article',
-         'read_aloud_play' => 'Play',
-         'read_aloud_pause' => 'Pause',
-         'read_aloud_seek' => 'Seek',
-         'read_aloud_speed' => 'Playback speed',
-         'article_subscribers_only' => 'Subscribers only',
-         'article_subscribe_button' => 'Subscribe',
-         'article_login_button' => 'Log in',
-         'article_already_subscriber' => 'Already a subscriber?',
-         'article_buy_lifetime_button' => 'Get lifetime access',
-         'subscription_one_time' => 'one-time payment',
+         'tag_no_products' => 'No published products with this tag yet.',
          'subscription_per_month' => 'month',
          'subscription_per_year' => 'year',
+         'page_title_not_found' => 'Not found',
+         'pagination_label' => 'Pages',
+         'product_rating_aria' => 'Average rating {rating} out of 5',
+         'antibot_image_label' => 'Security code',
+         'checkout_discount_name' => 'Discount',
+         'subscription_status_unpaid' => 'unpaid',
+         'subscription_status_incomplete' => 'waiting for payment',
+         'subscription_status_incomplete_expired' => 'expired',
+         'subscription_status_paused' => 'paused',
      ];
  
      public static function ensureKeys(array $defaults): void
⋮ 33 unchanged lines ⋮
includes/pagination.php Modified +1 -1
⋮ 52 unchanged lines ⋮
  
      private static function renderNumbered(string $baseUrl, int $currentPage, int $totalPages, array $extraParams): string
      {
-         $html = '<nav class="pagination-numbered" aria-label="Pagination">';
+         $html = '<nav class="pagination-numbered" aria-label="' . htmlspecialchars(Language::get('pagination_label', 'Pages'), ENT_QUOTES) . '">';
  
          if ($currentPage > 1) {
              $html .= '<a href="' . htmlspecialchars(self::buildUrl($baseUrl, $currentPage - 1, $extraParams)) . '" class="pagination-link pagination-prev">' . htmlspecialchars(Language::get('pagination_previous', 'Previous')) . '</a>';
⋮ 62 unchanged lines ⋮
includes/site-front.php Modified +58 -8
⋮ 12 unchanged lines ⋮
      private static ?array $settings = null;
  
      public const AD_PLACEMENTS = [
-         'header' => 'Header (below navigation, all pages)',
-         'homepage-top' => 'Homepage — top (below hero)',
-         'catalog-top' => 'Catalog grid — between cards (homepage & category)',
-         'product-page' => 'Product page — after gallery, before description',
+         'header' => 'Under the menu, on every page',
+         'homepage-top' => 'Home page, above featured products',
+         'catalog-top' => 'Between products in lists',
+         'product-page' => 'Product page, under the photos',
          'checkout' => 'Checkout page',
-         'footer' => 'Footer (above footer)',
+         'footer' => 'Just above the footer',
      ];
  
      public static function productUrl(string $slug, ?string $lang = null): string
⋮ 202 unchanged lines ⋮
  
              if ($row['item_type'] === 'page') {
                  $statement = Database::site()->prepare(
-                     "SELECT id, title, slug FROM pages WHERE id = :id AND show_in_menu = 1 AND status = 'published' LIMIT 1"
+                     "SELECT id, title, slug FROM pages WHERE id = :id AND show_in_menu = 1 AND status = 'published' AND is_home = 0 LIMIT 1"
                  );
                  $statement->execute(['id' => $row['ref_id']]);
                  $page = $statement->fetch();
⋮ 83 unchanged lines ⋮
  
          try {
              $pages = Database::site()->query(
-                 "SELECT id, title, slug FROM pages WHERE show_in_menu = 1 AND status = 'published' ORDER BY sort_order ASC, title ASC"
+                 "SELECT id, title, slug FROM pages WHERE show_in_menu = 1 AND status = 'published' AND is_home = 0 ORDER BY sort_order ASC, title ASC"
              )->fetchAll();
          } catch (PDOException $e) {
              $pages = [];
⋮ 124 unchanged lines ⋮
      public static function menuPages(): array
      {
          return Database::site()->query(
-             "SELECT title, slug FROM pages WHERE status = 'published' AND show_in_menu = 1 ORDER BY sort_order ASC, title ASC"
+             "SELECT title, slug FROM pages WHERE status = 'published' AND show_in_menu = 1 AND is_home = 0 ORDER BY sort_order ASC, title ASC"
          )->fetchAll();
+     }
+ 
+     public static function homePage(): ?array
+     {
+         require_once __DIR__ . '/block-editor.php';
+ 
+         $db = Database::site();
+         $row = $db->query('SELECT * FROM pages WHERE is_home = 1 ORDER BY id ASC LIMIT 1')->fetch();
+         if ($row) {
+             return $row;
+         }
+ 
+         $settings = self::settings();
+         $locked = false;
+ 
+         try {
+             $locked = (int) $db->query("SELECT GET_LOCK('stocketbase_home_page', 5)")->fetchColumn() === 1;
+ 
+             $row = $db->query('SELECT * FROM pages WHERE is_home = 1 ORDER BY id ASC LIMIT 1')->fetch();
+             if (!$row) {
+                 $slugCheck = $db->prepare('SELECT COUNT(*) FROM pages WHERE slug = :slug');
+                 $slug = '_home';
+                 for ($attempt = 2; $attempt < 100; $attempt++) {
+                     $slugCheck->execute(['slug' => $slug]);
+                     if ((int) $slugCheck->fetchColumn() === 0) {
+                         break;
+                     }
+                     $slug = '_home-' . $attempt;
+                 }
+ 
+                 $insert = $db->prepare(
+                     "INSERT INTO pages (author_id, title, slug, content_blocks, status, show_in_menu, sort_order, is_home) VALUES (NULL, :title, :slug, :content, 'published', 0, -1000, 1)"
+                 );
+                 $insert->execute([
+                     'title' => 'Home page',
+                     'slug' => $slug,
+                     'content' => BlockEditor::defaultHomepageBlocks(trim((string) ($settings['site_description'] ?? '')) !== ''),
+                 ]);
+ 
+                 $row = $db->query('SELECT * FROM pages WHERE is_home = 1 ORDER BY id ASC LIMIT 1')->fetch();
+             }
+         } catch (PDOException $exception) {
+             $row = false;
+         } finally {
+             if ($locked) {
+                 $db->query("SELECT RELEASE_LOCK('stocketbase_home_page')");
+             }
+         }
+ 
+         return $row ?: null;
      }
  
      public static function categories(): array
⋮ 343 unchanged lines ⋮
includes/status-label.php Added +44 -0
+ <?php
+ 
+ declare(strict_types=1);
+ 
+ final class StatusLabel
+ {
+     private const LABELS = [
+         'draft' => 'Draft',
+         'pending_review' => 'Waiting for approval',
+         'scheduled' => 'Scheduled',
+         'published' => 'Live',
+         'rejected' => 'Sent back',
+         'archived' => 'Archived',
+         'pending' => 'Waiting for payment',
+         'paid' => 'Paid',
+         'processing' => 'Being prepared',
+         'fulfilled' => 'Ready',
+         'shipped' => 'Shipped',
+         'completed' => 'Completed',
+         'canceled' => 'Canceled',
+         'cancelled' => 'Canceled',
+         'refunded' => 'Refunded',
+         'failed' => 'Payment failed',
+         'approved' => 'Approved',
+         'active' => 'Active',
+         'suspended' => 'Blocked',
+         'trialing' => 'Free trial',
+         'past_due' => 'Payment overdue',
+         'unpaid' => 'Unpaid',
+         'expired' => 'Expired',
+         'inactive' => 'Inactive',
+     ];
+ 
+     public static function get(string $status): string
+     {
+         $key = strtolower(trim($status));
+         if (isset(self::LABELS[$key])) {
+             return self::LABELS[$key];
+         }
+ 
+         return ucfirst(str_replace('_', ' ', $key));
+     }
+ }
+ 
index.php Modified +54 -38
⋮ 142 unchanged lines ⋮
  $pageDescription = $settings['seo_description'] !== '' ? $settings['seo_description'] : $settings['site_description'];
  $pageType = 'index';
  
- require __DIR__ . '/includes/front-header.php';
+ require_once __DIR__ . '/includes/block-editor.php';
  
- ?>
- <?php if (!empty($settings['site_description'])): ?>
-     <section class="hero-section">
-         <div class="hero-eyebrow"><?= htmlspecialchars(Language::get('nav_home', 'Home')) ?></div>
-         <h1 class="hero-title"><?= htmlspecialchars($settings['site_name']) ?></h1>
-         <p class="hero-subtitle"><?= htmlspecialchars($settings['site_description']) ?></p>
-     </section>
- <?php endif; ?>
+ try {
+     $homePage = SiteFront::homePage();
+ } catch (Throwable $exception) {
+     $homePage = null;
+ }
  
- <?php foreach (SiteFront::activeAds('homepage-top') as $homepageTopAd): ?>
-     <?= SiteFront::renderAd($homepageTopAd) ?>
- <?php endforeach; ?>
+ if ($homePage !== null) {
+     $homePage = Languages::pages([$homePage])[0];
+     $homeBlocksJson = (string) $homePage['content_blocks'];
+ } else {
+     $homeBlocksJson = BlockEditor::defaultHomepageBlocks(trim((string) ($settings['site_description'] ?? '')) !== '');
+ }
  
- <?php if (($settings['featured_banner_enabled'] ?? '0') === '1'): ?>
-     <div class="section-heading-row">
-         <h2 class="section-heading"><?= htmlspecialchars(Language::get('home_featured', 'Featured')) ?></h2>
-     </div>
+ $renderHomeFeatured = static function () use ($settings, $featuredProducts): string {
+     ob_start();
+     foreach (SiteFront::activeAds('homepage-top') as $homepageTopAd) {
+         echo SiteFront::renderAd($homepageTopAd);
+     }
+     if (($settings['featured_banner_enabled'] ?? '0') === '1') {
+         echo '<div class="section-heading-row"><h2 class="section-heading">' . htmlspecialchars(Language::get('home_featured', 'Featured')) . '</h2></div>';
+         if (!empty($featuredProducts)) {
+             echo renderFeaturedBanner($featuredProducts);
+         }
+     }
  
-     <?php if (!empty($featuredProducts)): ?>
-         <?= renderFeaturedBanner($featuredProducts) ?>
-     <?php endif; ?>
- <?php endif; ?>
+     return (string) ob_get_clean();
+ };
  
- <div class="article-grid" id="article-list" data-pagination-source="home">
-     <?php $catalogTopAds = SiteFront::activeAds('catalog-top'); ?>
-     <?php foreach ($products as $inFeedIndex => $product): ?>
-         <?= renderProductCard($product) ?>
-         <?php if ($inFeedIndex === 2 && !empty($catalogTopAds)): ?>
-             <?php foreach ($catalogTopAds as $catalogTopAd): ?>
-                 <?= SiteFront::renderAd($catalogTopAd) ?>
-             <?php endforeach; ?>
-         <?php endif; ?>
-     <?php endforeach; ?>
-     <?php if (empty($products)): ?>
-         <p><?= htmlspecialchars(Language::get('home_no_products', 'No products published yet.')) ?></p>
-     <?php endif; ?>
- </div>
+ $renderHomeProducts = static function () use ($settings, $products, $isStaticProductLoading, $currentPage, $totalPages): string {
+     ob_start();
+     $catalogTopAds = SiteFront::activeAds('catalog-top');
+     echo '<div class="article-grid" id="article-list" data-pagination-source="home">';
+     foreach ($products as $inFeedIndex => $product) {
+         echo renderProductCard($product);
+         if ($inFeedIndex === 2 && !empty($catalogTopAds)) {
+             foreach ($catalogTopAds as $catalogTopAd) {
+                 echo SiteFront::renderAd($catalogTopAd);
+             }
+         }
+     }
+     if (empty($products)) {
+         echo '<p>' . htmlspecialchars(Language::get('home_no_products', 'No products published yet.')) . '</p>';
+     }
+     echo '</div>';
+     echo '<div id="pagination-container">';
+     echo $isStaticProductLoading ? '' : Pagination::render($settings['pagination_style'], 'index.php', $currentPage, $totalPages, [], 'article-list');
+     echo '</div>';
  
- <div id="pagination-container">
-     <?= $isStaticProductLoading ? '' : Pagination::render($settings['pagination_style'], 'index.php', $currentPage, $totalPages, [], 'article-list') ?>
- </div>
+     return (string) ob_get_clean();
+ };
  
- <?php require __DIR__ . '/includes/front-footer.php'; ?>
+ require __DIR__ . '/includes/front-header.php';
+ 
+ echo BlockEditor::renderHomepage($homeBlocksJson, $settings, [
+     'home_featured' => $renderHomeFeatured,
+     'home_products' => $renderHomeProducts,
+ ]);
+ 
+ require __DIR__ . '/includes/front-footer.php';
  
interface.php Modified +17 -17
⋮ 23 unchanged lines ⋮
  }
  
  $dashActivePage = 'interface';
- $dashPageTitle = 'Interface';
+ $dashPageTitle = 'Appearance';
  
  require __DIR__ . '/includes/dash-header.php';
  
  ?>
  
- <h1 class="dash-title"><?= Icons::icon("sliders", "icon icon-lg") ?>Interface</h1>
+ <h1 class="dash-title"><?= Icons::icon("sliders", "icon icon-lg") ?>Appearance</h1>
  
  <div class="settings-tabs">
-     <a href="interface.php?tab=appearance" class="<?= $activeTab === 'appearance' ? 'active' : '' ?>"><?= Icons::icon("palette", "icon icon-sm") ?>Appearance</a>
-     <a href="interface.php?tab=shortcuts" class="<?= $activeTab === 'shortcuts' ? 'active' : '' ?>"><?= Icons::icon("keyboard", "icon icon-sm") ?>Shortcuts</a>
+     <a href="interface.php?tab=appearance" class="<?= $activeTab === 'appearance' ? 'active' : '' ?>"><?= Icons::icon("palette", "icon icon-sm") ?>Look</a>
+     <a href="interface.php?tab=shortcuts" class="<?= $activeTab === 'shortcuts' ? 'active' : '' ?>"><?= Icons::icon("keyboard", "icon icon-sm") ?>Keyboard shortcuts</a>
  </div>
  
  <?php if ($activeTab === 'appearance'): ?>
      <div class="settings-section">
-         <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Personal preferences for how the dashboard and product editor look and behave on this device — text size, fonts, editor helpers. Everything here is stored in this browser only and applies just to you; it's never saved to the server or shared with other team members.</p>
+         <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Make the dashboard comfortable for you. These choices are saved only in this browser, so they don't affect anyone else on your team.</p>
  
          <label>Sidebar text size</label>
          <select id="appearance-sidebar-scale" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
⋮ 7 unchanged lines ⋮
              <option value="150">150%</option>
          </select>
  
-         <label>Main content text size</label>
+         <label>Page text size</label>
          <select id="appearance-main-scale" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
              <option value="80">80%</option>
              <option value="90">90%</option>
⋮ 5 unchanged lines ⋮
              <option value="150">150%</option>
          </select>
  
-         <label>Editor interface font</label>
+         <label>Dashboard font</label>
          <select id="appearance-ui-font" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
              <option value="">Default</option>
              <?php foreach ($interfaceAvailableFonts as $interfaceFont): ?>
⋮ 1 unchanged line ⋮
              <?php endforeach; ?>
          </select>
  
-         <label>Content font (product editor text)</label>
+         <label>Font while writing products</label>
          <select id="appearance-content-font" style="width:100%;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--panel);color:var(--text);margin:6px 0 18px;">
              <option value="">Default</option>
              <?php foreach ($interfaceAvailableFonts as $interfaceFont): ?>
⋮ 1 unchanged line ⋮
              <?php endforeach; ?>
          </select>
  
-         <label style="margin-top:18px;"><input type="checkbox" id="appearance-glass"> Glass effect (translucent buttons and blurred backgrounds). Turn off for solid colors.</label>
-         <label><input type="checkbox" id="appearance-minimal"> Minimalism (removes all transitions, animations and gradients; also turns the glass effect off). Recommended for older computers and devices.</label>
-         <label><input type="checkbox" id="appearance-block-icons"> Show block type icons in the product editor</label>
-         <label><input type="checkbox" id="appearance-spellcheck"> Show browser spell check underlines in the product editor</label>
-         <label><input type="checkbox" id="appearance-paste-cleanup"> Always strip formatting when pasting into the product editor (Ctrl+Shift+V always does this regardless)</label>
+         <label style="margin-top:18px;"><input type="checkbox" id="appearance-glass"> Glass look (see-through buttons and soft blur). Turn off for plain colors.</label>
+         <label><input type="checkbox" id="appearance-minimal"> Simple mode (no animations or effects). Good for older computers.</label>
+         <label><input type="checkbox" id="appearance-block-icons"> Show little icons next to each block in the editor</label>
+         <label><input type="checkbox" id="appearance-spellcheck"> Underline spelling mistakes in the editor</label>
+         <label><input type="checkbox" id="appearance-paste-cleanup"> Paste as plain text in the editor (Ctrl+Shift+V always does this)</label>
  
          <?php if ($interfaceAiAvailable): ?>
-             <label><input type="checkbox" id="appearance-ai-helper"> Show the AI helper button in the product and page editors</label>
+             <label><input type="checkbox" id="appearance-ai-helper"> Show the AI assistant button in the editor</label>
          <?php endif; ?>
  
          <div class="publish-actions">
⋮ 202 unchanged lines ⋮
      </style>
  
      <div class="settings-section">
-         <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Keyboard shortcuts for the product editor. Stored in this browser only.</p>
+         <p style="font-size:12.5px;color:var(--muted);margin:-4px 0 20px;">Shortcuts for the editor. They're saved only in this browser.</p>
  
          <div id="shortcuts-list"></div>
  
⋮ 31 unchanged lines ⋮
                      comboBtn.type = 'button';
                      comboBtn.className = 'shortcut-combo-btn';
                      comboBtn.textContent = comboLabel(entry.combo);
-                     comboBtn.title = 'Click, then press a new key combination';
+                     comboBtn.title = 'Click, then press the keys you want';
  
                      var resetBtn = document.createElement('button');
                      resetBtn.type = 'button';
⋮ 22 unchanged lines ⋮
                  }
                  captureRow = btn;
                  btn.classList.add('capturing');
-                 btn.textContent = 'Press a key…';
+                 btn.textContent = 'Press the keys…';
  
                  function onKeydown(event) {
                      event.preventDefault();
⋮ 43 unchanged lines ⋮
languages.php Modified +33 -33
⋮ 19 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = (string) ($_POST['action'] ?? '');
⋮ 5 unchanged lines ⋮
              $defaultName = trim(strip_tags((string) ($_POST['multilang_default_name'] ?? '')));
  
              if (!Languages::isValidCode($defaultCode)) {
-                 $flashMessage = 'The original language code must be 2-3 letters, optionally with a region such as pt-br.';
+                 $flashMessage = 'The language code should be 2 or 3 letters, like en, pl or pt-br.';
                  $flashType = 'error';
              } elseif (Languages::find($defaultCode) !== null) {
-                 $flashMessage = 'The original language code is already used by one of the added languages.';
+                 $flashMessage = 'You already added a language with that code.';
                  $flashType = 'error';
              } else {
                  Languages::setSetting('multilang_enabled', isset($_POST['multilang_enabled']) ? '1' : '0');
⋮ 1 unchanged line ⋮
                  Languages::setSetting('multilang_default_name', $defaultName !== '' ? mb_substr($defaultName, 0, 60) : 'English');
                  Languages::setSetting('multilang_switcher', isset($_POST['multilang_switcher']) ? '1' : '0');
                  ActivityLog::record('language.settings');
-                 $flashMessage = 'Saved.';
+                 $flashMessage = 'Changes saved.';
              }
          } elseif ($action === 'add_language') {
              $error = Languages::addLanguage((string) ($_POST['code'] ?? ''), (string) ($_POST['name'] ?? ''));
⋮ 2 unchanged lines ⋮
                  $flashType = 'error';
              } else {
                  ActivityLog::record('language.add', null, null, $postedCode);
-                 $flashMessage = 'Language added. Translate the interface, then activate it.';
+                 $flashMessage = 'Language added. Translate the texts, then switch it on.';
              }
          } elseif ($action === 'toggle_language' && $knownCode) {
              $statement = $db->prepare('UPDATE site_languages SET is_active = 1 - is_active WHERE code = :code');
              $statement->execute(['code' => $postedCode]);
              ActivityLog::record('language.toggle', null, null, $postedCode);
-             $flashMessage = 'Saved.';
+             $flashMessage = 'Changes saved.';
          } elseif ($action === 'delete_language' && $knownCode) {
              $statement = $db->prepare('DELETE FROM site_languages WHERE code = :code');
              $statement->execute(['code' => $postedCode]);
              ActivityLog::record('language.delete', null, null, $postedCode);
-             $flashMessage = 'Language removed. Its translated files and texts are kept, so adding it again restores them.';
+             $flashMessage = 'Language removed. The translations are kept, so they come back if you add it again.';
          } elseif ($action === 'save_strings' && $knownCode) {
              $submitted = is_array($_POST['strings'] ?? null) ? $_POST['strings'] : [];
              if (Languages::saveStrings($postedCode, $submitted)) {
                  ActivityLog::record('language.strings', null, null, $postedCode);
-                 $flashMessage = 'Saved.';
+                 $flashMessage = 'Changes saved.';
              } else {
-                 $flashMessage = 'Could not write the translation file. Check that the translations folder is writable.';
+                 $flashMessage = 'We couldn\'t save the translation file. Ask your host to make the translations folder writable.';
                  $flashType = 'error';
              }
          } elseif ($action === 'save_site' && $knownCode) {
              Languages::saveSiteValues($postedCode, is_array($_POST['site'] ?? null) ? $_POST['site'] : []);
              ActivityLog::record('language.site', null, null, $postedCode);
-             $flashMessage = 'Saved.';
+             $flashMessage = 'Changes saved.';
          } elseif ($action === 'copy_site' && $knownCode) {
              $added = Languages::copySiteMissing($postedCode);
              ActivityLog::record('language.site_copy', null, null, $postedCode);
              $flashMessage = $added > 0
                  ? $added . ' missing text' . ($added === 1 ? '' : 's') . ' copied from the original. Existing translations were not changed.'
-                 : 'Nothing to copy: every text already has a translation.';
+                 : 'Everything is already translated.';
          } elseif ($action === 'copy_missing' && $knownCode) {
              $added = Languages::copyMissingStrings($postedCode);
              ActivityLog::record('language.copy', null, null, $postedCode);
              $flashMessage = $added > 0
                  ? $added . ' missing text' . ($added === 1 ? '' : 's') . ' copied from the original language. Existing translations were not changed.'
-                 : 'Nothing to copy: every text already has a translation.';
+                 : 'Everything is already translated.';
          } else {
-             $flashMessage = 'Unknown action.';
+             $flashMessage = 'Something went wrong. Please try again.';
              $flashType = 'error';
          }
  
⋮ 37 unchanged lines ⋮
  <div class="settings-tabs">
      <a href="languages.php" class="<?= $editing === null && $siteEditing === null ? 'active' : '' ?>"><?= Icons::icon('settings', 'icon icon-sm') ?>Overview</a>
      <?php if ($siteEditing !== null): ?>
-         <a href="languages.php?site=<?= urlencode($siteEditing['code']) ?>" class="active"><?= Icons::icon('globe', 'icon icon-sm') ?>Site texts: <?= htmlspecialchars($siteEditing['name']) ?></a>
+         <a href="languages.php?site=<?= urlencode($siteEditing['code']) ?>" class="active"><?= Icons::icon('globe', 'icon icon-sm') ?>Store texts: <?= htmlspecialchars($siteEditing['name']) ?></a>
      <?php endif; ?>
      <?php if ($editing !== null): ?>
          <a href="languages.php?edit=<?= urlencode($editing['code']) ?>" class="active"><?= Icons::icon('translate', 'icon icon-sm') ?>Interface: <?= htmlspecialchars($editing['name']) ?></a>
⋮ 6 unchanged lines ⋮
          <?= Csrf::field() ?>
          <input type="hidden" name="action" value="save_settings">
  
-         <label><input type="checkbox" name="multilang_enabled" <?= $multilangEnabled ? 'checked' : '' ?>> Enable multiple languages (adds a language switcher and language versions of the site under /<em>code</em>/)</label>
+         <label><input type="checkbox" name="multilang_enabled" <?= $multilangEnabled ? 'checked' : '' ?>> Offer your store in more than one language</label>
  
          <div class="settings-grid">
              <div>
-                 <label>Original language code</label>
+                 <label>Main language code</label>
                  <input type="text" name="multilang_default_code" value="<?= htmlspecialchars($defaultCode) ?>" maxlength="10" spellcheck="false">
              </div>
              <div>
-                 <label>Original language name</label>
+                 <label>Main language name</label>
                  <input type="text" name="multilang_default_name" value="<?= htmlspecialchars($defaultName) ?>" maxlength="60">
              </div>
          </div>
-         <p style="font-size:12.5px;color:var(--muted);margin:6px 0 0;">The original language is the one your products and pages are written in. It stays at the normal address, without a prefix.</p>
+         <p style="font-size:12.5px;color:var(--muted);margin:6px 0 0;">This is the language you write your products and pages in. It keeps your normal web address.</p>
  
-         <label><input type="checkbox" name="multilang_switcher" <?= $switcherEnabled ? 'checked' : '' ?>> Show the language switcher in the site footer</label>
+         <label><input type="checkbox" name="multilang_switcher" <?= $switcherEnabled ? 'checked' : '' ?>> Show a language picker in the footer</label>
  
          <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
      </form>
⋮ 4 unchanged lines ⋮
          <thead>
              <tr>
                  <th>Language</th>
-                 <th>Address</th>
-                 <th>Interface</th>
+                 <th>Web address</th>
+                 <th>Store texts</th>
                  <th>Status</th>
                  <th></th>
              </tr>
⋮ 2 unchanged lines ⋮
              <tr>
                  <td><?= htmlspecialchars($defaultName) ?> <span style="color:var(--muted);">(<?= htmlspecialchars($defaultCode) ?>)</span></td>
                  <td>/</td>
-                 <td>Original</td>
-                 <td><span class="status-pill status-published">original</span></td>
+                 <td>Normal address</td>
+                 <td><span class="status-pill status-published">Main language</span></td>
                  <td></td>
              </tr>
              <?php foreach ($languages as $language): ?>
⋮ 2 unchanged lines ⋮
                      <td><?= htmlspecialchars($language['name']) ?> <span style="color:var(--muted);">(<?= htmlspecialchars($language['code']) ?>)</span></td>
                      <td>/<?= htmlspecialchars($language['code']) ?>/</td>
                      <td><?= $missing === 0 ? 'Complete' : $missing . ' texts missing' ?></td>
-                     <td><span class="status-pill status-<?= (int) $language['is_active'] === 1 ? 'published' : 'draft' ?>"><?= (int) $language['is_active'] === 1 ? 'active' : 'inactive' ?></span></td>
+                     <td><span class="status-pill status-<?= (int) $language['is_active'] === 1 ? 'published' : 'draft' ?>"><?= (int) $language['is_active'] === 1 ? 'Visible' : 'Hidden' ?></span></td>
                      <td class="dash-table-actions">
                          <a href="languages.php?edit=<?= urlencode($language['code']) ?>" class="dash-btn-small"><?= Icons::icon('edit', 'icon icon-sm') ?>Interface</a>
-                         <a href="languages.php?site=<?= urlencode($language['code']) ?>" class="dash-btn-small"><?= Icons::icon('globe', 'icon icon-sm') ?>Site texts</a>
+                         <a href="languages.php?site=<?= urlencode($language['code']) ?>" class="dash-btn-small"><?= Icons::icon('globe', 'icon icon-sm') ?>Store texts</a>
                          <form method="post" style="display:inline;">
                              <?= Csrf::field() ?>
                              <input type="hidden" name="action" value="toggle_language">
                              <input type="hidden" name="code" value="<?= htmlspecialchars($language['code'], ENT_QUOTES) ?>">
-                             <button type="submit" class="dash-btn-small"><?= Icons::icon((int) $language['is_active'] === 1 ? 'x' : 'check', 'icon icon-sm') ?><?= (int) $language['is_active'] === 1 ? 'Deactivate' : 'Activate' ?></button>
+                             <button type="submit" class="dash-btn-small"><?= Icons::icon((int) $language['is_active'] === 1 ? 'x' : 'check', 'icon icon-sm') ?><?= (int) $language['is_active'] === 1 ? 'Hide' : 'Show' ?></button>
                          </form>
-                         <form method="post" style="display:inline;" onsubmit="return confirm('Remove this language? Its translations are kept and come back if you add it again.');">
+                         <form method="post" style="display:inline;" onsubmit="return confirm('Remove this language? Don\'t worry, the translations come back if you add it again.');">
                              <?= Csrf::field() ?>
                              <input type="hidden" name="action" value="delete_language">
                              <input type="hidden" name="code" value="<?= htmlspecialchars($language['code'], ENT_QUOTES) ?>">
⋮ 11 unchanged lines ⋮
          <input type="hidden" name="action" value="add_language">
          <div class="settings-grid">
              <div>
-                 <label>Code (e.g. pl, de, pt-br)</label>
+                 <label>Code (like pl, de or pt-br)</label>
                  <input type="text" name="code" maxlength="10" required spellcheck="false" autocomplete="off">
              </div>
              <div>
-                 <label>Name shown to visitors (e.g. Polski)</label>
+                 <label>Name customers see (like Polski)</label>
                  <input type="text" name="name" maxlength="60" required>
              </div>
          </div>
-         <p style="font-size:12.5px;color:var(--muted);margin:6px 0 0;">The language is created inactive. Translate the interface and your content first, then activate it. Products, pages and categories are translated from their edit screens.</p>
+         <p style="font-size:12.5px;color:var(--muted);margin:6px 0 0;">New languages start hidden. Translate the store texts and your content, then switch it on. You translate products, pages and categories from their own edit pages.</p>
          <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:20px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add language</button>
      </form>
  
⋮ 6 unchanged lines ⋮
      <?php $editMissing = Languages::missingStringCount($editing['code']); ?>
  
      <div class="sidebar-box" style="max-width:640px;margin-bottom:20px;">
-         <p style="margin:0 0 12px;">Interface texts for <strong><?= htmlspecialchars($editing['name']) ?></strong> are stored in <code>translations/language-<?= htmlspecialchars($editing['code']) ?>.php</code>. A text left empty falls back to the original language.</p>
+         <p style="margin:0 0 12px;">Store texts in <strong><?= htmlspecialchars($editing['name']) ?></strong>. Anything you leave empty shows in your main language.</p>
          <form method="post">
              <?= Csrf::field() ?>
              <input type="hidden" name="action" value="copy_missing">
              <input type="hidden" name="code" value="<?= htmlspecialchars($editing['code'], ENT_QUOTES) ?>">
-             <button type="submit" class="dash-btn" <?= $editMissing === 0 ? 'disabled' : '' ?>><?= Icons::icon('copy', 'icon icon-sm') ?>Copy missing texts from the original (<?= $editMissing ?>)</button>
+             <button type="submit" class="dash-btn" <?= $editMissing === 0 ? 'disabled' : '' ?>><?= Icons::icon('copy', 'icon icon-sm') ?>Fill empty texts with the original (<?= $editMissing ?>)</button>
          </form>
-         <p style="font-size:12.5px;color:var(--muted);margin:10px 0 0;">Only empty texts are filled in. Texts you already translated are never overwritten.</p>
+         <p style="font-size:12.5px;color:var(--muted);margin:10px 0 0;">Only empty fields get filled, your translations stay as they are.</p>
      </div>
  
      <form method="post" class="settings-section" style="max-width:none;">
⋮ 21 unchanged lines ⋮
llms.php Modified +1 -1
⋮ 33 unchanged lines ⋮
      echo "\n";
  }
  
- $pages = $db->query("SELECT title, slug FROM pages WHERE status = 'published' ORDER BY title ASC")->fetchAll();
+ $pages = $db->query("SELECT title, slug FROM pages WHERE status = 'published' AND is_home = 0 ORDER BY title ASC")->fetchAll();
  if (!empty($pages)) {
      echo "## Pages\n\n";
      foreach ($pages as $page) {
⋮ 18 unchanged lines ⋮
media-library.php Modified +1 -1
⋮ 13 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
      http_response_code(405);
-     echo json_encode(['success' => false, 'error' => 'Invalid request.']);
+     echo json_encode(['success' => false, 'error' => 'Something went wrong. Please refresh the page and try again.']);
      exit;
  }
  
⋮ 29 unchanged lines ⋮
media-upload.php Modified +5 -5
⋮ 13 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !Csrf::verify($_POST['csrf_token'] ?? null)) {
      http_response_code(403);
-     echo json_encode(['success' => false, 'error' => 'Invalid request.']);
+     echo json_encode(['success' => false, 'error' => 'Something went wrong. Please refresh the page and try again.']);
      exit;
  }
  
  if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
      http_response_code(400);
-     echo json_encode(['success' => false, 'error' => 'Upload failed.']);
+     echo json_encode(['success' => false, 'error' => 'The upload didn\'t work. Please try again.']);
      exit;
  }
  
⋮ 2 unchanged lines ⋮
  
  if ($file['size'] > $maxBytes) {
      http_response_code(400);
-     echo json_encode(['success' => false, 'error' => 'File too large (max 25MB).']);
+     echo json_encode(['success' => false, 'error' => 'That file is too big. The limit is 25 MB.']);
      exit;
  }
  
⋮ 11 unchanged lines ⋮
  
  if (!isset($allowedTypes[$mimeType])) {
      http_response_code(400);
-     echo json_encode(['success' => false, 'error' => 'Unsupported file type.']);
+     echo json_encode(['success' => false, 'error' => 'That file type isn\'t supported. Use JPG, PNG, GIF, WebP, MP4 or WebM.']);
      exit;
  }
  
⋮ 9 unchanged lines ⋮
  
  if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
      http_response_code(500);
-     echo json_encode(['success' => false, 'error' => 'Could not save file.']);
+     echo json_encode(['success' => false, 'error' => 'We couldn\'t save the file. Please try again.']);
      exit;
  }
  
⋮ 5 unchanged lines ⋮
page.php Modified +6 -1
⋮ 21 unchanged lines ⋮
  
  if (!$page) {
      http_response_code(404);
-     $pageTitle = 'Not found - ' . $settings['site_name'];
+     $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
      require __DIR__ . '/includes/front-header.php';
      echo '<p>' . htmlspecialchars(Language::get('page_not_found', 'Page not found.')) . '</p>';
      require __DIR__ . '/includes/front-footer.php';
+     exit;
+ }
+ 
+ if ((int) ($page['is_home'] ?? 0) === 1) {
+     header('Location: index.php', true, 301);
      exit;
  }
  
⋮ 22 unchanged lines ⋮
product.php Modified +6 -6
⋮ 50 unchanged lines ⋮
  }
  
  $query = $isPreview
-     ? "SELECT p.*, c.name AS category_name, c.slug AS category_slug, t.display_name AS author_name
+     ? "SELECT p.*, c.name AS category_name, c.slug AS category_slug, COALESCE(t.display_name, 'System') AS author_name
         FROM products p
         LEFT JOIN categories c ON c.id = p.category_id
-        JOIN team_accounts t ON t.id = p.created_by
+        LEFT JOIN team_accounts t ON t.id = p.created_by
         WHERE p.id = :id LIMIT 1"
-     : "SELECT p.*, c.name AS category_name, c.slug AS category_slug, t.display_name AS author_name
+     : "SELECT p.*, c.name AS category_name, c.slug AS category_slug, COALESCE(t.display_name, 'System') AS author_name
         FROM products p
         LEFT JOIN categories c ON c.id = p.category_id
-        JOIN team_accounts t ON t.id = p.created_by
+        LEFT JOIN team_accounts t ON t.id = p.created_by
         WHERE p.slug = :slug AND p.status = 'published' LIMIT 1";
  $statement = $db->prepare($query);
  $statement->execute($isPreview ? ['id' => $previewId] : ['slug' => $slug]);
⋮ 1 unchanged line ⋮
  
  if (!$product) {
      http_response_code(404);
-     $pageTitle = 'Not found - ' . $settings['site_name'];
+     $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
      require __DIR__ . '/includes/front-header.php';
      echo '<p>' . htmlspecialchars(Language::get('product_not_found', 'Product not found.')) . '</p>';
      require __DIR__ . '/includes/front-footer.php';
⋮ 161 unchanged lines ⋮
      <h1 class="article-title"><?= htmlspecialchars($product['title']) ?></h1>
  
      <?php if ($averageRating > 0): ?>
-         <div class="product-rating-summary" aria-label="Average rating <?= htmlspecialchars(number_format($averageRating, 1)) ?> out of 5">
+         <div class="product-rating-summary" aria-label="<?= htmlspecialchars(str_replace('{rating}', number_format($averageRating, 1), Language::get('product_rating_aria', 'Average rating {rating} out of 5')), ENT_QUOTES) ?>">
              <?php for ($star = 1; $star <= 5; $star++): ?><?= Icons::icon('star', 'icon star' . ($star <= round($averageRating) ? ' star-filled' : '')) ?><?php endfor; ?>
              <span class="product-rating-count"><?= htmlspecialchars(number_format($averageRating, 1)) ?> (<?= count($reviews) ?>)</span>
          </div>
⋮ 230 unchanged lines ⋮
search.php Modified +1 -1
⋮ 18 unchanged lines ⋮
  
  if ($settings['search_enabled'] !== '1') {
      http_response_code(404);
-     $pageTitle = 'Not found - ' . $settings['site_name'];
+     $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
      require __DIR__ . '/includes/front-header.php';
      echo '<p>' . htmlspecialchars(Language::get('search_not_available', 'Search is not available.')) . '</p>';
      require __DIR__ . '/includes/front-footer.php';
⋮ 145 unchanged lines ⋮
settings.php Modified

Not shown (binary file or too large to diff).

sitemap.php Modified +1 -1
⋮ 16 unchanged lines ⋮
  $baseUrl = rtrim(Config::get('APP_URL', ''), '/') . Config::get('APP_BASE_PATH', '');
  
  $products = $db->query("SELECT id, slug, updated_at FROM products WHERE status = 'published' ORDER BY updated_at DESC")->fetchAll();
- $pages = $db->query("SELECT slug, updated_at FROM pages WHERE status = 'published'")->fetchAll();
+ $pages = $db->query("SELECT slug, updated_at FROM pages WHERE status = 'published' AND is_home = 0")->fetchAll();
  $categories = $db->query('SELECT slug FROM categories')->fetchAll();
  
  header('Content-Type: application/xml; charset=utf-8');
⋮ 30 unchanged lines ⋮
sql/schema_site.sql Modified +9 -8
⋮ 50 unchanged lines ⋮
  
  CREATE TABLE IF NOT EXISTS products (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-     created_by INT UNSIGNED NOT NULL,
+     created_by INT UNSIGNED NULL,
      category_id INT UNSIGNED NULL,
      title VARCHAR(255) NOT NULL,
      slug VARCHAR(255) NOT NULL,
⋮ 26 unchanged lines ⋮
      KEY idx_product_category (category_id),
      KEY idx_product_status (status),
      KEY idx_product_published (published_at),
-     CONSTRAINT fk_product_author FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE CASCADE,
+     CONSTRAINT fk_product_author FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE SET NULL,
      CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  
⋮ 25 unchanged lines ⋮
  
  CREATE TABLE IF NOT EXISTS pages (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-     author_id INT UNSIGNED NOT NULL,
+     author_id INT UNSIGNED NULL,
      title VARCHAR(255) NOT NULL,
      slug VARCHAR(255) NOT NULL,
      content_blocks LONGTEXT NOT NULL,
      status ENUM('draft','published') NOT NULL DEFAULT 'draft',
      show_in_menu TINYINT(1) NOT NULL DEFAULT 0,
      sort_order INT NOT NULL DEFAULT 0,
+     is_home TINYINT(1) NOT NULL DEFAULT 0,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      UNIQUE KEY uq_page_slug (slug),
-     CONSTRAINT fk_page_author FOREIGN KEY (author_id) REFERENCES team_accounts(id) ON DELETE CASCADE
+     CONSTRAINT fk_page_author FOREIGN KEY (author_id) REFERENCES team_accounts(id) ON DELETE SET NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  
  CREATE TABLE IF NOT EXISTS site_settings (
⋮ 10 unchanged lines ⋮
      content LONGTEXT NOT NULL,
      link_url VARCHAR(255) NULL,
      is_active TINYINT(1) NOT NULL DEFAULT 1,
-     created_by INT UNSIGNED NOT NULL,
+     created_by INT UNSIGNED NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-     CONSTRAINT fk_ad_creator FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE CASCADE
+     CONSTRAINT fk_ad_creator FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE SET NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  
  CREATE TABLE IF NOT EXISTS analytics_scripts (
⋮ 35 unchanged lines ⋮
      starts_at DATETIME NULL,
      ends_at DATETIME NULL,
      is_active TINYINT(1) NOT NULL DEFAULT 1,
-     created_by INT UNSIGNED NOT NULL,
+     created_by INT UNSIGNED NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      UNIQUE KEY uq_discount_code (code),
-     CONSTRAINT fk_discount_creator FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE CASCADE
+     CONSTRAINT fk_discount_creator FOREIGN KEY (created_by) REFERENCES team_accounts(id) ON DELETE SET NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  
  CREATE TABLE IF NOT EXISTS product_files (
⋮ 19 unchanged lines ⋮
tag.php Modified +1 -1
⋮ 20 unchanged lines ⋮
  
  if ($tag === null) {
      http_response_code(404);
-     $pageTitle = 'Not found - ' . $settings['site_name'];
+     $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
      require __DIR__ . '/includes/front-header.php';
      echo '<p>' . htmlspecialchars(Language::get('tag_not_found', 'Tag not found.')) . '</p>';
      require __DIR__ . '/includes/front-footer.php';
⋮ 59 unchanged lines ⋮
team-login.php Modified +7 -7
⋮ 39 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $error = 'Security check failed, please try again.';
+         $error = 'Your session expired. Please try again.';
      } elseif ($pending !== null) {
          $identifier = (string) $pending['id'];
          $wait = LoginThrottle::secondsUntilAllowed('2fa', $identifier);
⋮ 7 unchanged lines ⋮
              exit;
          } else {
              LoginThrottle::recordFailure('2fa', $identifier);
-             $error = 'Invalid code.';
+             $error = 'That code didn\'t work. Please try again.';
          }
      } elseif (!AntiBot::verify('team', $_POST['antibot_answer'] ?? null, $_POST['antibot_started'] ?? null, $_POST['website'] ?? null)) {
-         $error = 'Security verification failed, please try again.';
+         $error = 'The security code wasn\'t right. Please try again.';
      } else {
          $username = trim((string) ($_POST['username'] ?? ''));
          $password = (string) ($_POST['password'] ?? '');
  
          if ($username === '' || $password === '') {
-             $error = 'Please enter your username and password.';
+             $error = 'Please type your username and password.';
          } else {
              $wait = LoginThrottle::secondsUntilAllowed('team', $username);
  
⋮ 4 unchanged lines ⋮
  
                  if ($account === null) {
                      LoginThrottle::recordFailure('team', $username);
-                     $error = 'Invalid credentials.';
+                     $error = 'That username or password isn\'t right.';
                  } else {
                      LoginThrottle::clear('team', $username);
  
⋮ 42 unchanged lines ⋮
  </script>
  <div class="auth-card">
  <h1>Team login</h1>
- <p class="auth-subtitle"><?= htmlspecialchars($siteName) ?> editorial access</p>
+ <p class="auth-subtitle">Sign in to manage <?= htmlspecialchars($siteName) ?></p>
  
  <?php if ($error !== null): ?>
  <div class="auth-error"><?= htmlspecialchars($error) ?></div>
⋮ 2 unchanged lines ⋮
  <?php if ($pending !== null): ?>
  <form method="post">
  <?= Csrf::field() ?>
- <label>Authentication code (or a recovery code)</label>
+ <label>Code from your app (or a backup code)</label>
  <input type="text" name="code" required autofocus autocomplete="one-time-code" inputmode="text" maxlength="16" spellcheck="false">
  <button type="submit">Verify</button>
  <p style="margin-top:14px;font-size:13px;"><a href="team-login.php?cancel=1">Back to login</a></p>
⋮ 21 unchanged lines ⋮
translate.php Modified +20 -16
⋮ 47 unchanged lines ⋮
  
  if (!$canTranslate) {
      http_response_code(403);
-     exit('You do not have permission to translate this item.');
+     exit('You don\'t have access to translate this.');
  }
  
  $originalTitle = (string) ($type === 'category' ? $original['name'] : $original['title']);
  $originalExcerpt = (string) ($type === 'category' ? ($original['description'] ?? '') : ($original['excerpt'] ?? ''));
  $originalBlocks = (string) ($original['content_blocks'] ?? '');
+ $isHomeSource = $type === 'page' && (int) ($original['is_home'] ?? 0) === 1;
  $originalSeoTitle = (string) ($original['seo_title'] ?? '');
  $originalSeoDescription = (string) ($original['seo_description'] ?? '');
  
⋮ 22 unchanged lines ⋮
  
  if ($noLanguages === false && $_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = (string) ($_POST['action'] ?? '');
⋮ 28 unchanged lines ⋮
              $fields = [
                  'title' => $title,
                  'excerpt' => in_array('excerpt', $source['fields'], true) ? trim((string) ($_POST['excerpt'] ?? '')) : '',
-                 'content_blocks' => in_array('blocks', $source['fields'], true) ? BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), $type === 'page') : '',
+                 'content_blocks' => in_array('blocks', $source['fields'], true) ? BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), $type === 'page' && !$isHomeSource, $isHomeSource) : '',
                  'seo_title' => in_array('seo_title', $source['fields'], true) ? mb_substr(trim((string) ($_POST['seo_title'] ?? '')), 0, 255) : '',
                  'seo_description' => in_array('seo_description', $source['fields'], true) ? mb_substr(trim((string) ($_POST['seo_description'] ?? '')), 0, 500) : '',
              ];
  
              if ($title === '') {
-                 $flashMessage = 'The translated title is required. To remove the translation use "Remove translation".';
+                 $flashMessage = 'Please fill in the title. If you want to drop this translation, use "Remove translation" instead.';
                  $flashType = 'error';
              } else {
                  Languages::saveTranslation($type, $entityId, $lang, $fields);
⋮ 7 unchanged lines ⋮
  
  $done = (string) ($_GET['done'] ?? '');
  if ($flashMessage === null && $done !== '') {
-     $flashMessage = ['saved' => 'Translation saved.', 'copied' => 'Missing fields copied from the original. Existing translations were not changed.', 'deleted' => 'Translation removed.'][$done] ?? null;
+     $flashMessage = ['saved' => 'Translation saved.', 'copied' => 'Empty fields were filled with the original text. Nothing you already translated was changed.', 'deleted' => 'Translation removed.'][$done] ?? null;
  }
  
  $translation = $noLanguages ? null : Languages::translation($type, $entityId, $lang);
⋮ 11 unchanged lines ⋮
  $formSeoTitle = (string) ($translation['seo_title'] ?? '');
  $formSeoDescription = (string) ($translation['seo_description'] ?? '');
  $formBlocks = $translation !== null && !$blocksAreEmpty((string) ($translation['content_blocks'] ?? '')) ? (string) $translation['content_blocks'] : '{"blocks":[]}';
+ if ($isHomeSource) {
+     $formBlocks = BlockEditor::sanitize($blocksAreEmpty($formBlocks) ? $originalBlocks : $formBlocks, false, true);
+ }
  $hasBlocks = in_array('blocks', $source['fields'], true);
  
  $dashActivePage = $type === 'category' ? 'categories' : '';
⋮ 19 unchanged lines ⋮
  
  <div class="dash-header-row">
      <h1 class="dash-title"><?= Icons::icon('translate', 'icon icon-lg') ?>Translate <?= htmlspecialchars($source['label']) ?></h1>
-     <a href="<?= htmlspecialchars($source['back'] . $entityId, ENT_QUOTES) ?>" class="dash-btn"><?= Icons::icon('arrow-up', 'icon icon-sm') ?>Back to original</a>
+     <a href="<?= htmlspecialchars($source['back'] . $entityId, ENT_QUOTES) ?>" class="dash-btn"><?= Icons::icon('arrow-up', 'icon icon-sm') ?>Back</a>
  </div>
  
  <?php if ($flashMessage !== null): ?>
⋮ 2 unchanged lines ⋮
  
  <?php if ($noLanguages): ?>
      <div class="sidebar-box" style="max-width:640px;">
-         <p style="margin:0 0 12px;">No languages have been added yet.</p>
+         <p style="margin:0 0 12px;">You haven't added any languages yet.</p>
          <?php if ($isEditor): ?>
              <a href="languages.php" class="dash-btn dash-btn-primary"><?= Icons::icon('translate', 'icon icon-sm') ?>Add a language</a>
          <?php else: ?>
-             <p style="margin:0;color:var(--muted);">Ask an editor in chief to add one in Languages.</p>
+             <p style="margin:0;color:var(--muted);">Ask the store owner to add one in Languages.</p>
          <?php endif; ?>
      </div>
  <?php else: ?>
⋮ 8 unchanged lines ⋮
  
      <p style="color:var(--muted);margin:0 0 18px;">Original (<?= htmlspecialchars(Languages::defaultName()) ?>): <strong><?= htmlspecialchars($originalTitle) ?></strong>
          &middot; Translating to <strong><?= htmlspecialchars((string) $currentLanguage['name']) ?></strong>
-         <?php if ((int) $currentLanguage['is_active'] !== 1): ?>&middot; <span class="status-pill status-draft">language inactive</span><?php endif; ?></p>
+         <?php if ((int) $currentLanguage['is_active'] !== 1): ?>&middot; <span class="status-pill status-draft">hidden from customers</span><?php endif; ?></p>
  
      <form method="post" id="article-form" class="article-form">
          <?= Csrf::field() ?>
⋮ 15 unchanged lines ⋮
  
              <?php if ($hasBlocks): ?>
                  <label><?= Icons::icon('grid', 'icon icon-sm') ?>Content</label>
-                 <div id="block-editor-root" data-initial-blocks='<?= htmlspecialchars($formBlocks, ENT_QUOTES) ?>'></div>
+                 <div id="block-editor-root" data-initial-blocks='<?= htmlspecialchars($formBlocks, ENT_QUOTES) ?>'<?= $isHomeSource ? ' data-home="1"' : '' ?>></div>
                  <?php if (!$blocksAreEmpty($originalBlocks)): ?>
                      <details class="sidebar-box" style="margin-top:14px;">
-                         <summary style="cursor:pointer;font-weight:600;">Show original content</summary>
+                         <summary style="cursor:pointer;font-weight:600;">Show the original text</summary>
                          <div class="article-content" style="margin-top:12px;"><?= BlockEditor::render($originalBlocks, false) ?></div>
                      </details>
                  <?php endif; ?>
              <?php endif; ?>
  
              <?php if (in_array('seo_title', $source['fields'], true)): ?>
-                 <label>SEO title</label>
+                 <label>Title in Google</label>
                  <input type="text" name="seo_title" value="<?= htmlspecialchars($formSeoTitle) ?>" maxlength="255">
                  <?= $hint($originalSeoTitle) ?>
-                 <label>SEO description</label>
+                 <label>Description in Google</label>
                  <textarea name="seo_description" rows="2" maxlength="500"><?= htmlspecialchars($formSeoDescription) ?></textarea>
                  <?= $hint($originalSeoDescription) ?>
              <?php endif; ?>
⋮ 9 unchanged lines ⋮
              <?= Csrf::field() ?>
              <input type="hidden" name="action" value="copy">
              <input type="hidden" name="lang" value="<?= htmlspecialchars($lang, ENT_QUOTES) ?>">
-             <button type="submit" class="dash-btn"><?= Icons::icon('copy', 'icon icon-sm') ?>Copy missing fields from the original</button>
+             <button type="submit" class="dash-btn"><?= Icons::icon('copy', 'icon icon-sm') ?>Fill empty fields with the original</button>
          </form>
          <?php if ($translation !== null): ?>
-             <form method="post" onsubmit="return confirm('Remove this translation? The original is not affected.');">
+             <form method="post" onsubmit="return confirm('Remove this translation? The original stays as it is.');">
                  <?= Csrf::field() ?>
                  <input type="hidden" name="action" value="delete">
                  <input type="hidden" name="lang" value="<?= htmlspecialchars($lang, ENT_QUOTES) ?>">
⋮ 1 unchanged line ⋮
              </form>
          <?php endif; ?>
      </div>
-     <p style="font-size:12.5px;color:var(--muted);margin:10px 0 0;">Copying only fills fields that are still empty. Anything already translated stays as it is. Fields left empty show the original text to visitors.</p>
+     <p style="font-size:12.5px;color:var(--muted);margin:10px 0 0;">Filling only touches empty fields, your translations stay. Anything left empty shows the original text to customers.</p>
  
  <?php endif; ?>
  
⋮ 2 unchanged lines ⋮
translations/language.php Modified +9 -0
⋮ 270 unchanged lines ⋮
      'subscription_status_past_due' => 'payment overdue',
      'subscription_status_canceled' => 'canceled',
      'subscription_status_expired' => 'expired',
+     'subscription_status_unpaid' => 'unpaid',
+     'subscription_status_incomplete' => 'waiting for payment',
+     'subscription_status_incomplete_expired' => 'expired',
+     'subscription_status_paused' => 'paused',
  
      'email_order_subject' => 'Order confirmation — {site} #{order}',
      'email_order_heading' => 'Thanks for your order!',
⋮ 32 unchanged lines ⋮
      'contact_email_page' => 'Page',
      'contact_email_date' => 'Date',
      'contact_email_reply_hint' => 'Reply to this email to answer the sender directly.',
+     'page_title_not_found' => 'Not found',
+     'pagination_label' => 'Pages',
+     'product_rating_aria' => 'Average rating {rating} out of 5',
+     'antibot_image_label' => 'Security code',
+     'checkout_discount_name' => 'Discount',
  ];
  
updater.php Modified +56 -56
⋮ 21 unchanged lines ⋮
  
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
-         $flashMessage = 'Security check failed, please try again.';
+         $flashMessage = 'Your session expired. Please try again.';
          $flashType = 'error';
      } else {
          $action = $_POST['action'] ?? '';
  
          if ($action === 'toggle_updater') {
              if ($lockedByConfig) {
-                 $flashMessage = 'The updater is disabled in the server configuration.';
+                 $flashMessage = 'Updates are switched off in your server settings.';
                  $flashType = 'error';
              } else {
                  $turnOn = ($_POST['enabled'] ?? '0') === '1';
                  Updater::setEnabled($db, $turnOn);
-                 $flashMessage = $turnOn ? 'Updater enabled.' : 'Updater disabled.';
+                 $flashMessage = $turnOn ? 'Updates turned on.' : 'Updates turned off.';
              }
          }
  
          if ($action === 'save_auto_update' || $action === 'regenerate_cron_token') {
              if (!$canInstall) {
-                 $flashMessage = 'Only the Super Admin can manage automatic updates.';
+                 $flashMessage = 'Only the Super Admin can change automatic updates.';
                  $flashType = 'error';
              } elseif (!Updater::enabled($db)) {
-                 $flashMessage = 'Turn on the updater first.';
+                 $flashMessage = 'Turn on updates first.';
                  $flashType = 'error';
              } elseif ($action === 'regenerate_cron_token') {
                  Updater::regenerateCronToken($db);
-                 $flashMessage = 'A new cron token was generated. Update the URL in your cron job — the old one no longer works.';
+                 $flashMessage = 'New link created. Put it in your cron job, the old one won\'t work anymore.';
              } else {
                  $turnOnAuto = ($_POST['auto_enabled'] ?? '0') === '1';
                  if ($turnOnAuto && ($_POST['auto_risk_ack'] ?? '') !== '1') {
-                     $flashMessage = 'To turn on automatic updates, confirm that you understand the risks.';
+                     $flashMessage = 'Please tick the box to confirm you understand the risks first.';
                      $flashType = 'error';
                  } else {
                      Updater::saveAutoSettings($db, $turnOnAuto, ($_POST['auto_htaccess'] ?? '0') === '1');
-                     $flashMessage = $turnOnAuto ? 'Automatic updates turned on. Add the cron job below to run them.' : 'Automatic updates turned off.';
+                     $flashMessage = $turnOnAuto ? 'Automatic updates are on. Add the cron job below so they actually run.' : 'Automatic updates are off.';
                  }
              }
          }
  
          if ($action === 'check_updates' || $action === 'download_release') {
              if (!Updater::enabled($db)) {
-                 $flashMessage = 'The updater is disabled.';
+                 $flashMessage = 'Updates are turned off.';
                  $flashType = 'error';
              } else {
                  session_write_close();
⋮ 9 unchanged lines ⋮
  
          if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
              if (!$canInstall) {
-                 $flashMessage = 'Only the Super Admin can install updates or manage backups.';
+                 $flashMessage = 'Only the Super Admin can install updates or use backups.';
                  $flashType = 'error';
              } elseif ($action === 'install_release' && !Updater::enabled($db)) {
-                 $flashMessage = 'The updater is disabled.';
+                 $flashMessage = 'Updates are turned off.';
                  $flashType = 'error';
              } else {
                  session_write_close();
⋮ 3 unchanged lines ⋮
                  if ($action === 'install_release') {
                      $outcome = Updater::install($currentVersion, ($_POST['update_htaccess'] ?? '1') === '1');
                      $flashMessage = $outcome['ok']
-                         ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files). A backup was saved as ' . $outcome['backup'] . '.'
+                         ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files changed). We saved a backup first: ' . $outcome['backup'] . '.'
                          : $outcome['error'];
                  } elseif ($action === 'restore_backup') {
                      $outcome = Updater::restore($backupId);
⋮ 2 unchanged lines ⋮
                          : $outcome['error'];
                  } else {
                      $outcome = ['ok' => Updater::deleteBackup($backupId)];
-                     $flashMessage = $outcome['ok'] ? 'Backup deleted.' : 'Backup not found.';
+                     $flashMessage = $outcome['ok'] ? 'Backup deleted.' : 'We couldn\'t find that backup.';
                  }
  
                  $flashType = $outcome['ok'] ? 'success' : 'error';
⋮ 32 unchanged lines ⋮
      <div class="dash-card">
          <?= Icons::icon('layers') ?>
          <div class="dash-card-value"><?= htmlspecialchars($currentVersion) ?></div>
-         <div class="dash-card-label">Installed version</div>
+         <div class="dash-card-label">Your version</div>
      </div>
      <div class="dash-card">
          <?= Icons::icon('toggle') ?>
-         <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></div>
-         <div class="dash-card-label">Updater status</div>
+         <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'On' : 'Off' ?></span></div>
+         <div class="dash-card-label">Updates</div>
      </div>
  </div>
  
  <?php if ($lockedByConfig): ?>
-     <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The updater is turned off in the server configuration (UPDATER_DISABLED=1) and sends no requests.</p>
+     <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Updates are switched off in your server settings (UPDATER_DISABLED=1), so this site never checks for them.</p>
  <?php else: ?>
      <div class="publish-actions">
          <?php if ($updaterEnabled): ?>
⋮ 8 unchanged lines ⋮
              <input type="hidden" name="action" value="toggle_updater">
              <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
              <?php if ($updaterEnabled): ?>
-                 <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Disable updater</button>
+                 <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Turn off updates</button>
              <?php else: ?>
-                 <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Enable updater</button>
+                 <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Turn on updates</button>
              <?php endif; ?>
          </form>
      </div>
      <?php if (!$updaterEnabled): ?>
-         <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The updater is off by default. While it is off, this site sends no requests to the update server.</p>
+         <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Updates are off until you turn them on. While they're off, your store never contacts the update server.</p>
      <?php endif; ?>
  
      <?php if ($updaterEnabled): ?>
-         <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates (cron)</h2>
+         <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates</h2>
  
          <div class="updater-auto-warning" role="alert">
-             <strong><?= Icons::icon('shield', 'icon icon-sm') ?>Warning: automatic updates can be dangerous for your store.</strong>
+             <strong><?= Icons::icon('shield', 'icon icon-sm') ?>Heads up: automatic updates can break your store.</strong>
              <ul>
-                 <li>New versions are downloaded and installed <strong>without any human review or confirmation</strong>.</li>
-                 <li>An update can <strong>overwrite or change features, templates and files</strong> — including your own edits to StocketBase files — and <strong>nobody will be asked or warned first</strong>.</li>
-                 <li>A broken or incompatible release can take the storefront, checkout or dashboard offline until someone restores a backup.</li>
-                 <li>Every automatic install makes a backup (kept for 30 days) that can be restored below, but restoring requires a person to log in.</li>
+                 <li>New versions get installed <strong>without anyone checking them first</strong>.</li>
+                 <li>An update can <strong>change or replace features and files</strong>, including your own edits, and <strong>nobody gets asked or warned</strong>.</li>
+                 <li>A bad release could take your store, checkout or dashboard down until someone restores a backup.</li>
+                 <li>We back up before every install and keep backups for 30 days, but someone still has to log in to restore one.</li>
              </ul>
-             <p>Leave this off unless you accept these risks. Manual updates above let you review every change before installing.</p>
+             <p>Leave this off unless you're OK with that. With manual updates you see every change before it goes in.</p>
          </div>
  
          <div class="dash-cards">
⋮ 5 unchanged lines ⋮
              <div class="dash-card">
                  <?= Icons::icon('clock') ?>
                  <div class="dash-card-value" style="font-size:14px;"><?= $autoUpdate['last_run'] !== null ? htmlspecialchars(substr((string) $autoUpdate['last_run']['at'], 0, 19)) : 'Never' ?></div>
-                 <div class="dash-card-label">Last cron run</div>
+                 <div class="dash-card-label">Last automatic check</div>
              </div>
          </div>
  
          <?php if ($autoUpdate['last_run'] !== null): ?>
-             <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Last result (<?= htmlspecialchars((string) $autoUpdate['last_run']['status']) ?>): <?= htmlspecialchars((string) $autoUpdate['last_run']['message']) ?></p>
+             <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Last time (<?= htmlspecialchars((string) $autoUpdate['last_run']['status']) ?>): <?= htmlspecialchars((string) $autoUpdate['last_run']['message']) ?></p>
          <?php endif; ?>
  
          <?php if ($canInstall): ?>
              <form method="post" class="settings-section" style="max-width:720px;">
                  <?= Csrf::field() ?>
                  <input type="hidden" name="action" value="save_auto_update">
-                 <label><input type="checkbox" name="auto_enabled" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new releases automatically when the cron job runs</label>
-                 <label><input type="checkbox" name="auto_htaccess" value="1" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also overwrite .htaccess files during automatic updates (leave unchecked to keep your own server rules)</label>
-                 <label><input type="checkbox" name="auto_risk_ack" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> I understand that automatic updates can break the store and overwrite features without anyone being informed</label>
-                 <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save automatic update settings</button>
+                 <label><input type="checkbox" name="auto_enabled" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new versions automatically</label>
+                 <label><input type="checkbox" name="auto_htaccess" value="1" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also replace .htaccess files (leave this off if you changed them yourself)</label>
+                 <label><input type="checkbox" name="auto_risk_ack" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> I understand automatic updates can break my store and change things without telling anyone</label>
+                 <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
              </form>
  
              <?php if ($autoUpdate['enabled']): ?>
                  <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Cron job</h2>
-                 <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Add one of these to your hosting's cron jobs (once a day is enough). Nothing happens until the cron job runs.</p>
-                 <label>Command line (recommended)</label>
+                 <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Add one of these as a cron job in your hosting panel. Once a day is plenty. Nothing happens until it runs.</p>
+                 <label>Command (best option)</label>
                  <input type="text" readonly value="<?= htmlspecialchars($cronCliCommand) ?>" onclick="this.select();">
-                 <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Run it as the same system user as the web server, otherwise updated files may end up with the wrong owner.</p>
-                 <label>URL (for hosts without command-line cron)</label>
+                 <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Run it as the same user as your website, or the new files may end up with the wrong permissions.</p>
+                 <label>Link (if your host can't run commands)</label>
                  <input type="text" readonly value="<?= htmlspecialchars('wget -q -O - "' . $cronHttpUrl . '"') ?>" onclick="this.select();">
-                 <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>The token in this URL lets anyone who knows it start an update. Keep it private.</p>
-                 <form method="post" onsubmit="return confirm('Generate a new token? The current cron URL will stop working.');">
+                 <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Anyone with this link can start an update, so keep it to yourself.</p>
+                 <form method="post" onsubmit="return confirm('Create a new link? The current one will stop working.');">
                      <?= Csrf::field() ?>
                      <input type="hidden" name="action" value="regenerate_cron_token">
-                     <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Generate new token</button>
+                     <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Create a new link</button>
                  </form>
              <?php endif; ?>
          <?php else: ?>
-             <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change automatic updates.</p>
+             <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change this.</p>
          <?php endif; ?>
      <?php endif; ?>
  <?php endif; ?>
⋮ 2 unchanged lines ⋮
      <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>Result</h2>
  
      <?php if ($result['status'] === 'update'): ?>
-         <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>Update available: <?= htmlspecialchars($result['remote_version']) ?><?= $result['released_at'] !== '' ? ' (' . htmlspecialchars($result['released_at']) . ')' : '' ?></div>
+         <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>New version available: <?= htmlspecialchars($result['remote_version']) ?><?= $result['released_at'] !== '' ? ' (' . htmlspecialchars($result['released_at']) . ')' : '' ?></div>
      <?php elseif ($result['status'] === 'current'): ?>
-         <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>You are running the latest version.</div>
+         <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>You're up to date.</div>
      <?php else: ?>
-         <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>This installation (<?= htmlspecialchars($currentVersion) ?>) is newer than the published version (<?= htmlspecialchars($result['remote_version']) ?>).</div>
+         <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>This installation (<?= htmlspecialchars($currentVersion) ?>) is newer than the latest release (<?= htmlspecialchars($result['remote_version']) ?>).</div>
      <?php endif; ?>
  
      <?php if (!empty($result['changelog'])): ?>
⋮ 9 unchanged lines ⋮
          <form method="post" class="publish-actions">
              <?= Csrf::field() ?>
              <input type="hidden" name="action" value="download_release">
-             <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>Download and show changes</button>
+             <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>See what's changed</button>
          </form>
-         <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The release is downloaded temporarily and compared with your files. Nothing is installed or changed.</p>
+         <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>We just compare the new version with your files. Nothing gets installed yet.</p>
      <?php elseif (empty($result['files'])): ?>
-         <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files are identical to the release.</p>
+         <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files already match this version.</p>
      <?php endif; ?>
  
      <?php
⋮ 7 unchanged lines ⋮
  
      <?php if ($result['compared'] && $result['status'] !== 'ahead' && !empty($result['files'])): ?>
          <?php if ($canInstall): ?>
-             <form method="post" class="publish-actions" onsubmit="return confirm('<?= $result['status'] === 'update' ? 'Install version' : 'Sync your files with version' ?> <?= htmlspecialchars($result['remote_version'], ENT_QUOTES) ?>? A backup of the files being replaced is saved first, and everything is rolled back if a file cannot be written.');">
+             <form method="post" class="publish-actions" onsubmit="return confirm('<?= $result['status'] === 'update' ? 'Install version' : 'Match your files to version' ?> <?= htmlspecialchars($result['remote_version'], ENT_QUOTES) ?>? We back up first, and undo everything if something goes wrong.');">
                  <?= Csrf::field() ?>
                  <input type="hidden" name="action" value="install_release">
                  <?php if ($htaccessCount > 0): ?>
                      <input type="hidden" name="update_htaccess" value="0">
-                     <label style="flex-basis:100%;"><input type="checkbox" name="update_htaccess" value="1" checked> Also update .htaccess files (<?= (int) $htaccessCount ?>). Uncheck to keep your own server rules.</label>
+                     <label style="flex-basis:100%;"><input type="checkbox" name="update_htaccess" value="1" checked> Also replace .htaccess files (<?= (int) $htaccessCount ?>). Untick if you changed them yourself.</label>
                  <?php endif; ?>
-                 <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?><?= $result['status'] === 'update' ? 'Install update' : 'Sync files with release' ?></button>
+                 <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?><?= $result['status'] === 'update' ? 'Install update' : 'Match my files' ?></button>
              </form>
-             <p class="updater-note"><?= Icons::icon('shield', 'icon icon-sm') ?>Files are backed up to updater-files/backups before being replaced. Backups older than 30 days are deleted automatically.</p>
+             <p class="updater-note"><?= Icons::icon('shield', 'icon icon-sm') ?>We back up your files before replacing anything. Backups are kept for 30 days.</p>
          <?php else: ?>
              <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can install updates.</p>
          <?php endif; ?>
      <?php endif; ?>
  
      <?php if (!empty($result['files'])): ?>
-         <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Files (<?= count($result['files']) ?>)</h2>
+         <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Changed files (<?= count($result['files']) ?>)</h2>
  
          <?php foreach ($result['files'] as $file): ?>
              <details class="updater-file">
⋮ 45 unchanged lines ⋮
          <?php endforeach; ?>
  
          <?php if ($result['skipped'] > 0): ?>
-             <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?><?= (int) $result['skipped'] ?> files were skipped because they are identical to yours or have an invalid path.</p>
+             <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?><?= (int) $result['skipped'] ?> files were skipped because they're already the same or couldn't be used.</p>
          <?php endif; ?>
      <?php endif; ?>
  <?php endif; ?>
⋮ 22 unchanged lines ⋮
                          <?php elseif ($backup['completed']): ?>
                              <span class="status-pill status-published">Installed</span>
                          <?php else: ?>
-                             <span class="status-pill status-rejected">Not completed</span>
+                             <span class="status-pill status-rejected">Didn't finish</span>
                          <?php endif; ?>
                      </td>
                      <td class="dash-table-actions">
                          <?php if ($canInstall): ?>
-                             <form method="post" onsubmit="return confirm('Restore the files from this backup? Files installed by the update will be replaced with the saved versions.');">
+                             <form method="post" onsubmit="return confirm('Restore this backup? Files from the update will be swapped back to the saved ones.');">
                                  <?= Csrf::field() ?>
                                  <input type="hidden" name="action" value="restore_backup">
                                  <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
                                  <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
                              </form>
-                             <form method="post" onsubmit="return confirm('Delete this backup permanently?');">
+                             <form method="post" onsubmit="return confirm('Delete this backup? You can\'t get it back.');">
                                  <?= Csrf::field() ?>
                                  <input type="hidden" name="action" value="delete_backup">
                                  <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
⋮ 12 unchanged lines ⋮
version.txt Modified +1 -1
- 1.0.0.0
+ 1.0.0.1