v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/user-auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/language.php';
- require_once __DIR__ . '/includes/site-front.php';
- require_once __DIR__ . '/includes/block-editor.php';
- require_once __DIR__ . '/includes/tags.php';
- require_once __DIR__ . '/includes/auth.php';
- require_once __DIR__ . '/includes/currency.php';
- require_once __DIR__ . '/includes/digital-files.php';
- require_once __DIR__ . '/includes/product-stats.php';
-
- $previewId = (int) ($_GET['preview'] ?? 0);
- $isPreview = $previewId > 0;
-
- $db = Database::site();
- $settings = SiteFront::settings();
-
- $slug = trim((string) ($_GET['slug'] ?? ''));
-
- if (Languages::enabled() && !isset($_GET['stocketbase_lang']) && preg_match('#^([a-z]{2,3}(?:-[a-z]{2,4})?)/(.*)$#', $slug, $languageMatch) === 1 && in_array($languageMatch[1], Languages::activeCodes(), true)) {
- $_GET['stocketbase_lang'] = $languageMatch[1];
- Languages::resetCache();
- SiteFront::resetSettings();
- $settings = SiteFront::settings();
- $languageRest = trim($languageMatch[2], '/');
-
- if ($languageRest === '') {
- require __DIR__ . '/index.php';
- exit;
- }
-
- if (preg_match('/^[a-z0-9-]+\.php$/', $languageRest) === 1 && $languageRest !== 'product.php' && is_file(__DIR__ . '/' . $languageRest)) {
- require __DIR__ . '/' . $languageRest;
- exit;
- }
-
- $slug = $languageRest;
- $_GET['slug'] = $languageRest;
- }
-
- if ($isPreview) {
- Auth::boot();
- } else {
- UserAuth::boot();
- }
-
- $query = $isPreview
- ? "SELECT p.*, c.name AS category_name, c.slug AS category_slug, t.display_name 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
- WHERE p.id = :id LIMIT 1"
- : "SELECT p.*, c.name AS category_name, c.slug AS category_slug, t.display_name 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
- WHERE p.slug = :slug AND p.status = 'published' LIMIT 1";
- $statement = $db->prepare($query);
- $statement->execute($isPreview ? ['id' => $previewId] : ['slug' => $slug]);
- $product = $statement->fetch();
-
- if (!$product) {
- http_response_code(404);
- $pageTitle = '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';
- exit;
- }
-
- if ($isPreview) {
- if (!Auth::check()) {
- http_response_code(403);
- exit('Preview access requires team login.');
- }
-
- $currentTeamUser = Auth::user();
- $canPreview = $currentTeamUser !== null
- && ((int) $product['created_by'] === (int) $currentTeamUser['id']
- || Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER)
- || Auth::role() === Auth::ROLE_CATALOG_ASSISTANT);
- if (!$canPreview) {
- http_response_code(403);
- exit('You do not have permission to preview this product.');
- }
- } else {
- $updateViews = $db->prepare('UPDATE products SET views_count = views_count + 1 WHERE id = :id');
- $updateViews->execute(['id' => $product['id']]);
- }
-
- $imagesStatement = $db->prepare('SELECT image_path, alt_text FROM product_images WHERE product_id = :id ORDER BY sort_order ASC, id ASC');
- $imagesStatement->execute(['id' => $product['id']]);
- $galleryImages = $imagesStatement->fetchAll();
- if (empty($galleryImages) && !empty($product['cover_image_path'])) {
- $galleryImages = [['image_path' => $product['cover_image_path'], 'alt_text' => null]];
- }
-
- $variantsStatement = $db->prepare('SELECT * FROM product_variants WHERE product_id = :id ORDER BY is_default DESC, sort_order ASC, id ASC');
- $variantsStatement->execute(['id' => $product['id']]);
- $variants = $variantsStatement->fetchAll();
-
- $productTags = ProductTags::isEnabled() ? ProductTags::forProduct((int) $product['id']) : [];
-
- $productFiles = DigitalFiles::filesForProduct((int) $product['id']);
- $productIsFree = DigitalFiles::isFree($product, $variants);
- $downloadLoginRequired = ($settings['require_account_to_purchase'] ?? '0') === '1';
- $downloadCustomer = $isPreview ? null : UserAuth::user();
- $customerOwnsProduct = !$productIsFree && !empty($productFiles) && $downloadCustomer !== null
- && DigitalFiles::customerOwnsProduct((int) $downloadCustomer['id'], (int) $product['id']);
-
- $productBuyersCount = ($settings['product_show_buyers'] ?? '0') === '1' ? ProductStats::buyersCount((int) $product['id']) : 0;
- $productDownloadsCount = ($settings['product_show_downloads'] ?? '0') === '1' && !empty($productFiles) ? ProductStats::downloadsCount((int) $product['id']) : 0;
-
- $reviewsDb = Database::users();
- $reviewsEnabled = ($settings['reviews_enabled'] ?? '1') === '1';
-
- $loadReviews = function () use ($reviewsDb, $product): array {
- $statement = $reviewsDb->prepare(
- "SELECT r.*, u.display_name FROM product_reviews r
- LEFT JOIN user_accounts u ON u.id = r.user_account_id
- WHERE r.product_id = :id AND r.status = 'visible'
- ORDER BY r.created_at DESC LIMIT 50"
- );
- $statement->execute(['id' => $product['id']]);
-
- return $statement->fetchAll();
- };
-
- $reviews = $loadReviews();
- $reviewError = null;
- $reviewSuccess = null;
-
- if (!$isPreview && $_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'submit_review') {
- if (!$reviewsEnabled) {
- $reviewError = Language::get('review_disabled', 'Reviews are currently disabled.');
- } elseif (!UserAuth::check()) {
- $reviewError = Language::get('review_login_required', 'Please log in to leave a review.');
- } elseif (!Csrf::verify($_POST['csrf_token'] ?? null)) {
- $reviewError = Language::get('form_security_failed', 'Security check failed, please try again.');
- } else {
- $rating = (int) ($_POST['rating'] ?? 0);
- $body = trim((string) ($_POST['body'] ?? ''));
-
- $existingReview = $reviewsDb->prepare('SELECT id FROM product_reviews WHERE product_id = :product_id AND user_account_id = :user_id LIMIT 1');
- $existingReview->execute(['product_id' => $product['id'], 'user_id' => UserAuth::user()['id']]);
-
- if ($existingReview->fetch()) {
- $reviewError = Language::get('review_already_posted', 'You have already reviewed this product.');
- } elseif ($rating < 1 || $rating > 5) {
- $reviewError = Language::get('review_rating_required', 'Please select a star rating from 1 to 5.');
- } elseif ($body === '') {
- $reviewError = Language::get('review_body_required', 'Please write a short review.');
- } else {
- $insert = $reviewsDb->prepare(
- "INSERT INTO product_reviews (product_id, user_account_id, rating, body, status) VALUES (:product_id, :user_id, :rating, :body, 'visible')"
- );
- $insert->execute([
- 'product_id' => $product['id'],
- 'user_id' => UserAuth::user()['id'],
- 'rating' => $rating,
- 'body' => mb_substr($body, 0, 2000),
- ]);
- $reviewSuccess = Language::get('review_posted', 'Thanks — your review has been posted.');
- $reviews = $loadReviews();
- }
- }
- }
-
- $averageRating = 0.0;
- if (!empty($reviews)) {
- $ratedReviews = array_filter($reviews, static fn (array $review): bool => $review['rating'] !== null);
- if (!empty($ratedReviews)) {
- $averageRating = array_sum(array_map(static fn (array $review): int => (int) $review['rating'], $ratedReviews)) / count($ratedReviews);
- }
- }
-
- $relatedProducts = [];
- $latestProducts = [];
-
- if (($settings['related_products_enabled'] ?? '1') === '1') {
- if (!empty($product['category_id'])) {
- $relatedStatement = $db->prepare(
- "SELECT id, title, slug, cover_image_path FROM products WHERE status = 'published' AND category_id = :category_id AND id != :id ORDER BY published_at DESC LIMIT 4"
- );
- $relatedStatement->execute(['category_id' => $product['category_id'], 'id' => $product['id']]);
- $relatedProducts = $relatedStatement->fetchAll();
- }
-
- $latestStatement = $db->prepare(
- "SELECT id, title, slug, cover_image_path FROM products WHERE status = 'published' AND id != :id ORDER BY published_at DESC LIMIT 4"
- );
- $latestStatement->execute(['id' => $product['id']]);
- $latestProducts = $latestStatement->fetchAll();
- }
-
- $pricingModel = (string) ($settings['pricing_model'] ?? 'per_product');
- $isSaasPolar = (string) ($settings['store_category'] ?? 'general') === 'saas' && (string) ($settings['payment_provider'] ?? 'stripe') === 'polar';
- $currentUrl = SiteFront::productUrl((string) $product['slug']);
- $productCurrency = (string) ($product['currency'] ?: $settings['store_currency']);
-
- $pageTitle = ($product['seo_title'] ?: $product['title']) . ' - ' . $settings['site_name'];
- $pageDescription = $product['seo_description'] ?: $product['excerpt'];
- $ogType = 'product';
- $ogImagePath = !empty($product['cover_image_path']) ? $product['cover_image_path'] : null;
- $pageType = 'product';
- $pageTitle = $isPreview ? 'Preview - ' . $pageTitle : $pageTitle;
-
- require __DIR__ . '/includes/front-header.php';
-
- ?>
- <article class="article-view product-view">
-
- <?php if (($settings['breadcrumbs_enabled'] ?? '0') === '1'): ?>
- <nav class="article-breadcrumbs" aria-label="<?= htmlspecialchars(Language::get('breadcrumb_label', 'Breadcrumb')) ?>">
- <ol>
- <li><a href="index.php"><?= htmlspecialchars(Language::get('nav_home', 'Home')) ?></a></li>
- <?php if (!empty($product['category_name'])): ?>
- <li><a href="category.php?slug=<?= urlencode($product['category_slug']) ?>"><?= htmlspecialchars($product['category_name']) ?></a></li>
- <?php endif; ?>
- <li aria-current="page"><span><?= htmlspecialchars($product['title']) ?></span></li>
- </ol>
- </nav>
- <?php endif; ?>
-
- <?php if (!empty($product['category_name'])): ?>
- <a href="category.php?slug=<?= urlencode($product['category_slug']) ?>" class="article-category-link"><?= htmlspecialchars($product['category_name']) ?></a>
- <?php endif; ?>
-
- <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">
- <?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>
- <?php endif; ?>
-
- <?= SiteFront::renderShareControl($product) ?>
-
- <?php if (!empty($galleryImages)): ?>
- <div class="product-gallery">
- <?php foreach ($galleryImages as $galleryIndex => $galleryImage): ?>
- <img src="<?= htmlspecialchars($galleryImage['image_path']) ?>" alt="<?= htmlspecialchars((string) ($galleryImage['alt_text'] ?? '')) ?>" class="product-gallery-image<?= $galleryIndex === 0 ? ' product-gallery-image-active' : '' ?>" data-gallery-index="<?= $galleryIndex ?>">
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
-
- <?php foreach (SiteFront::activeAds('product-page') as $productPageAd): ?>
- <?= SiteFront::renderAd($productPageAd) ?>
- <?php endforeach; ?>
-
- <div class="product-buy-box">
- <div class="product-price-row">
- <?php if (!empty($product['price_cents'])): ?>
- <span class="product-price"><?= htmlspecialchars(Currency::format((int) $product['price_cents'], $productCurrency)) ?></span>
- <?php if (!empty($product['compare_at_price_cents']) && (int) $product['compare_at_price_cents'] > (int) $product['price_cents']): ?>
- <span class="product-price-compare"><?= htmlspecialchars(Currency::format((int) $product['compare_at_price_cents'], $productCurrency)) ?></span>
- <?php endif; ?>
- <?php if (!empty($product['billing_interval']) && $product['billing_interval'] !== 'one_time'): ?>
- <span class="product-price-interval">/ <?= htmlspecialchars($product['billing_interval'] === 'year' ? Language::get('subscription_per_year', 'year') : Language::get('subscription_per_month', 'month')) ?></span>
- <?php endif; ?>
- <?php elseif ($productIsFree): ?>
- <span class="product-price"><?= htmlspecialchars(Language::get('product_free_label', 'Free')) ?></span>
- <?php endif; ?>
- </div>
-
- <?php if ($productBuyersCount > 0 || $productDownloadsCount > 0): ?>
- <div class="product-counters">
- <?php if ($productBuyersCount > 0): ?>
- <span class="product-counter"><?= Icons::icon('users', 'icon icon-sm') ?><?= htmlspecialchars(sprintf(Language::get('product_buyers_label', 'Buyers: %s'), number_format($productBuyersCount))) ?></span>
- <?php endif; ?>
- <?php if ($productDownloadsCount > 0): ?>
- <span class="product-counter"><?= Icons::icon('download', 'icon icon-sm') ?><?= htmlspecialchars(sprintf(Language::get('product_downloads_label', 'Downloads: %s'), number_format($productDownloadsCount))) ?></span>
- <?php endif; ?>
- </div>
- <?php endif; ?>
-
- <?php if (!$isPreview && !empty($productFiles) && ($productIsFree || $customerOwnsProduct)): ?>
- <div class="product-downloads">
- <?php if ($customerOwnsProduct): ?>
- <p class="product-downloads-owned"><?= Icons::icon('check', 'icon icon-sm') ?><?= htmlspecialchars(Language::get('product_owned_label', 'You own this product.')) ?></p>
- <?php endif; ?>
- <?php if ($productIsFree && $downloadLoginRequired && $downloadCustomer === null): ?>
- <a href="user-login.php?redirect=<?= urlencode('product.php?slug=' . rawurlencode((string) $product['slug'])) ?>" class="unlock-btn"><?= htmlspecialchars(Language::get('product_download_login', 'Log in to download')) ?></a>
- <?php else: ?>
- <?php foreach ($productFiles as $productFile): ?>
- <a href="<?= htmlspecialchars(DigitalFiles::downloadUrl((int) $productFile['id'])) ?>" class="unlock-btn product-download-btn" rel="nofollow">
- <?= Icons::icon('download', 'icon icon-sm') ?>
- <?php if (trim((string) ($productFile['button_label'] ?? '')) !== ''): ?>
- <span><?= htmlspecialchars(DigitalFiles::displayName($productFile)) ?></span>
- <?php else: ?>
- <span><?= htmlspecialchars(Language::get('product_download_button', 'Download')) ?> <?= htmlspecialchars($productFile['original_name']) ?></span>
- <?php endif; ?>
- <small><?= htmlspecialchars(DigitalFiles::formatSize((int) $productFile['size_bytes'])) ?></small>
- </a>
- <?php endforeach; ?>
- <?php endif; ?>
- </div>
- <?php endif; ?>
-
- <?php if (!empty($product['track_inventory'])): ?>
- <?php $stockQty = $product['stock_quantity'] !== null ? (int) $product['stock_quantity'] : null; ?>
- <div class="product-stock-status">
- <?php if ($stockQty !== null && $stockQty <= 0): ?>
- <span class="badge-out-of-stock"><?= htmlspecialchars(Language::get('product_out_of_stock', 'Out of stock')) ?></span>
- <?php elseif ($stockQty !== null && $stockQty <= 5): ?>
- <span class="badge-low-stock"><?= htmlspecialchars(sprintf(Language::get('product_low_stock', 'Only %d left'), $stockQty)) ?></span>
- <?php else: ?>
- <span class="badge-in-stock"><?= htmlspecialchars(Language::get('product_in_stock', 'In stock')) ?></span>
- <?php endif; ?>
- </div>
- <?php endif; ?>
-
- <?php if (!$isPreview && !$productIsFree): ?>
- <?php if ($isSaasPolar): ?>
- <form action="checkout.php" method="post" class="product-buy-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="provider" value="polar">
- <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;"><?= htmlspecialchars(Language::get('product_subscribe_polar_button', 'Subscribe via Polar')) ?></button>
- </form>
- <?php elseif ($pricingModel === 'subscription'): ?>
- <form action="checkout.php" method="post" class="product-buy-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;"><?= htmlspecialchars(Language::get('product_subscribe_button', 'Subscribe')) ?></button>
- </form>
- <?php else: ?>
- <?php $outOfStock = !empty($product['track_inventory']) && $product['stock_quantity'] !== null && (int) $product['stock_quantity'] <= 0 && empty($variants); ?>
- <form action="cart.php" method="post" class="product-buy-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="add">
- <input type="hidden" name="product_id" value="<?= (int) $product['id'] ?>">
- <input type="hidden" name="return_to" value="<?= htmlspecialchars($currentUrl, ENT_QUOTES) ?>">
- <?php if (!empty($variants)): ?>
- <label class="product-variant-label" for="variant_id"><?= htmlspecialchars(Language::get('product_variant_label', 'Options')) ?></label>
- <select name="variant_id" id="variant_id" class="product-variant-select">
- <?php foreach ($variants as $variant): ?>
- <option value="<?= (int) $variant['id'] ?>">
- <?= htmlspecialchars($variant['name']) ?><?= $variant['price_cents'] !== null ? ' — ' . htmlspecialchars(Currency::format((int) $variant['price_cents'], $productCurrency)) : '' ?>
- </option>
- <?php endforeach; ?>
- </select>
- <?php endif; ?>
- <label class="product-quantity-label" for="quantity"><?= htmlspecialchars(Language::get('product_quantity_label', 'Quantity')) ?></label>
- <input type="number" name="quantity" id="quantity" value="1" min="1" max="999" class="product-quantity-input">
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;" <?= $outOfStock ? 'disabled' : '' ?>><?= htmlspecialchars(Language::get('product_add_to_cart_button', 'Add to cart')) ?></button>
- </form>
- <?php endif; ?>
- <?php endif; ?>
- </div>
-
- <?php
- $productHtml = BlockEditor::render($product['content_blocks']);
- ?>
- <div class="article-content product-content">
- <?= $productHtml ?>
- </div>
-
- <?php if (!empty($productTags)): ?>
- <div class="article-tags">
- <span class="article-tags-label"><?= htmlspecialchars(Language::get('product_tags_label', 'Tags:')) ?></span>
- <?php foreach ($productTags as $productTag): ?>
- <a href="tag.php?slug=<?= urlencode($productTag['slug']) ?>" class="article-tag"><?= htmlspecialchars($productTag['name']) ?></a>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
-
- </article>
-
- <?php if (($settings['related_products_enabled'] ?? '1') === '1' && (!empty($relatedProducts) || !empty($latestProducts))): ?>
- <section class="related-section">
- <?php if (!empty($relatedProducts)): ?>
- <h2><?= htmlspecialchars(Language::get('product_related', 'Related products')) ?></h2>
- <div class="article-grid article-grid-small">
- <?php foreach ($relatedProducts as $related): ?>
- <a href="<?= htmlspecialchars(SiteFront::productUrl($related['slug'])) ?>" class="product-card">
- <?php if (!empty($related['cover_image_path'])): ?>
- <img src="<?= htmlspecialchars($related['cover_image_path']) ?>" alt="" class="product-card-image">
- <?php endif; ?>
- <div class="product-card-body">
- <h3><?= htmlspecialchars($related['title']) ?></h3>
- </div>
- </a>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
-
- <?php if (!empty($latestProducts)): ?>
- <h2><?= htmlspecialchars(Language::get('product_latest', 'More recent products')) ?></h2>
- <div class="article-grid article-grid-small">
- <?php foreach ($latestProducts as $latest): ?>
- <a href="<?= htmlspecialchars(SiteFront::productUrl($latest['slug'])) ?>" class="product-card">
- <?php if (!empty($latest['cover_image_path'])): ?>
- <img src="<?= htmlspecialchars($latest['cover_image_path']) ?>" alt="" class="product-card-image">
- <?php endif; ?>
- <div class="product-card-body">
- <h3><?= htmlspecialchars($latest['title']) ?></h3>
- </div>
- </a>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
- </section>
- <?php endif; ?>
-
- <?php if ($reviewsEnabled && !$isPreview): ?>
- <section class="reviews-section">
- <h2><?= htmlspecialchars(Language::get('product_reviews_title', 'Reviews')) ?></h2>
-
- <?php if (empty($reviews)): ?>
- <p><?= htmlspecialchars(Language::get('product_no_reviews', 'No reviews yet.')) ?></p>
- <?php else: ?>
- <ul class="reviews-list">
- <?php foreach ($reviews as $review): ?>
- <li class="review-item">
- <div class="review-header">
- <strong><?= htmlspecialchars((string) ($review['display_name'] ?? 'Customer')) ?></strong>
- <?php if ($review['rating'] !== null): ?>
- <span class="review-stars">
- <?php for ($star = 1; $star <= 5; $star++): ?><?= Icons::icon('star', 'icon star' . ($star <= (int) $review['rating'] ? ' star-filled' : '')) ?><?php endfor; ?>
- </span>
- <?php endif; ?>
- <span class="review-date"><?= htmlspecialchars(date('F j, Y', strtotime($review['created_at']))) ?></span>
- </div>
- <?php if (!empty($review['body'])): ?>
- <p class="review-body"><?= nl2br(htmlspecialchars($review['body'])) ?></p>
- <?php endif; ?>
- </li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
-
- <?php if ($reviewError !== null): ?>
- <p class="review-error" style="color: var(--danger);"><?= htmlspecialchars($reviewError) ?></p>
- <?php endif; ?>
- <?php if ($reviewSuccess !== null): ?>
- <p class="review-success" style="color: var(--success);"><?= htmlspecialchars($reviewSuccess) ?></p>
- <?php endif; ?>
-
- <?php if (UserAuth::check()): ?>
- <form method="post" class="review-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="submit_review">
- <label><?= htmlspecialchars(Language::get('product_review_rating_label', 'Your rating')) ?></label>
- <div class="review-rating-input">
- <?php for ($star = 5; $star >= 1; $star--): ?>
- <label aria-label="<?= htmlspecialchars(sprintf(Language::get('review_star_label', '%d / 5'), $star)) ?>">
- <input type="radio" name="rating" value="<?= $star ?>" required>
- <?= Icons::icon('star', 'icon') ?>
- </label>
- <?php endfor; ?>
- </div>
- <label for="review-body"><?= htmlspecialchars(Language::get('product_review_body_label', 'Your review')) ?></label>
- <textarea name="body" id="review-body" rows="4" maxlength="2000"></textarea>
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;"><?= htmlspecialchars(Language::get('product_review_submit_button', 'Submit review')) ?></button>
- </form>
- <?php else: ?>
- <p><a href="user-login.php?mode=login"><?= htmlspecialchars(Language::get('product_review_login_prompt', 'Log in to leave a review')) ?></a></p>
- <?php endif; ?>
- </section>
- <?php endif; ?>
-
- <?php require __DIR__ . '/includes/front-footer.php'; ?>
-