mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
15bcef262f
- Add contact.php (was 404, linked from footer and returns page) - Add shippingDetails and hasMerchantReturnPolicy to product schema - Add priceValidUntil to product Offer schema - Improve merchant feed descriptions (use DB description when present) - Add handling/transit times and return_policy_label to feed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
318 lines
16 KiB
PHP
318 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Tom's Java Jive - Product Detail Page
|
|
*/
|
|
|
|
$extraHead = '<link rel="stylesheet" href="/assets/css/products.css?v=' . filemtime(__DIR__ . '/assets/css/products.css') . '">';
|
|
require_once __DIR__ . '/includes/functions.php';
|
|
|
|
$productId = $_GET['id'] ?? '';
|
|
|
|
if (!$productId) {
|
|
header('Location: /shop.php');
|
|
exit;
|
|
}
|
|
|
|
// Get product
|
|
$product = db()->fetch(
|
|
"SELECT * FROM products WHERE product_id = :id AND is_active = 1",
|
|
['id' => $productId]
|
|
);
|
|
|
|
if (!$product) {
|
|
header('Location: /shop.php');
|
|
exit;
|
|
}
|
|
|
|
// Get product reviews
|
|
$reviews = db()->fetchAll(
|
|
"SELECT * FROM reviews WHERE product_id = :id AND is_approved = 1 ORDER BY created_at DESC LIMIT 10",
|
|
['id' => $productId]
|
|
);
|
|
|
|
$avgRating = 0;
|
|
$reviewCount = count($reviews);
|
|
if ($reviewCount > 0) {
|
|
$avgRating = array_sum(array_column($reviews, 'rating')) / $reviewCount;
|
|
}
|
|
|
|
// Get related products
|
|
$relatedProducts = db()->fetchAll(
|
|
"SELECT * FROM products WHERE category = :category AND product_id != :id AND is_active = 1 LIMIT 4",
|
|
['category' => $product['category'], 'id' => $productId]
|
|
);
|
|
|
|
$images = json_decode($product['images'] ?? '[]', true);
|
|
$mainImage = !empty($images) ? $images[0] : '/assets/images/placeholder-product.svg';
|
|
$salePrice = $product['sale_price'];
|
|
$price = $product['price'];
|
|
$inStock = $product['stock'] > 0;
|
|
$displayPrice = $salePrice ?? $price;
|
|
|
|
// SEO meta
|
|
$categoryLabel = ucfirst($product['category'] ?? 'Coffee');
|
|
$metaTitle = $product['name'] . ' ' . $categoryLabel . " Coffee | Tom's Java Jive";
|
|
$metaDescription = truncate(strip_tags($product['description'] ?? ''), 155)
|
|
?: $product['name'] . ' flavored artisan coffee beans freshly roasted in Weatherford, TX. Available in whole bean and ground. Ships nationwide.';
|
|
$metaKeywords = strtolower($product['name']) . ' coffee, ' . strtolower($categoryLabel) . ' coffee, flavored coffee beans, artisan coffee, buy coffee online';
|
|
$canonicalUrl = 'https://tomsjavajive.com/product.php?id=' . $productId;
|
|
$ogType = 'product';
|
|
$ogImage = (strpos($mainImage, 'http') === 0) ? $mainImage : 'https://tomsjavajive.com' . $mainImage;
|
|
|
|
// Structured data — Product schema
|
|
$productSchemaData = [
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'Product',
|
|
'name' => $product['name'] . ' ' . $categoryLabel . ' Coffee',
|
|
'url' => $canonicalUrl,
|
|
'image' => $ogImage,
|
|
'description' => $metaDescription,
|
|
'brand' => ['@type' => 'Brand', 'name' => "Tom's Java Jive"],
|
|
'offers' => [
|
|
'@type' => 'Offer',
|
|
'priceCurrency' => 'USD',
|
|
'price' => number_format((float) $displayPrice, 2, '.', ''),
|
|
'priceValidUntil' => date('Y-12-31', strtotime('+1 year')),
|
|
'availability' => $inStock ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
|
|
'url' => $canonicalUrl,
|
|
'seller' => ['@type' => 'Organization', 'name' => "Tom's Java Jive"],
|
|
'shippingDetails' => [
|
|
'@type' => 'OfferShippingDetails',
|
|
'shippingRate' => ['@type' => 'MonetaryAmount', 'value' => '5.99', 'currency' => 'USD'],
|
|
'shippingDestination'=> ['@type' => 'DefinedRegion', 'addressCountry' => 'US'],
|
|
'deliveryTime' => [
|
|
'@type' => 'ShippingDeliveryTime',
|
|
'handlingTime' => ['@type' => 'QuantitativeValue', 'minValue' => 1, 'maxValue' => 2, 'unitCode' => 'DAY'],
|
|
'transitTime' => ['@type' => 'QuantitativeValue', 'minValue' => 3, 'maxValue' => 7, 'unitCode' => 'DAY'],
|
|
],
|
|
],
|
|
'hasMerchantReturnPolicy' => [
|
|
'@type' => 'MerchantReturnPolicy',
|
|
'applicableCountry' => 'US',
|
|
'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
|
|
'merchantReturnDays' => 30,
|
|
'returnMethod' => 'https://schema.org/ReturnByMail',
|
|
'returnFees' => 'https://schema.org/FreeReturn',
|
|
'returnPolicyLink' => 'https://tomsjavajive.com/returns.php',
|
|
],
|
|
],
|
|
];
|
|
if ($reviewCount > 0) {
|
|
$productSchemaData['aggregateRating'] = [
|
|
'@type' => 'AggregateRating',
|
|
'ratingValue' => round($avgRating, 1),
|
|
'reviewCount' => $reviewCount,
|
|
'bestRating' => 5,
|
|
'worstRating' => 1,
|
|
];
|
|
}
|
|
$productSchema = json_encode($productSchemaData, JSON_UNESCAPED_SLASHES);
|
|
|
|
// Breadcrumbs
|
|
$breadcrumbs = [
|
|
['name' => 'Home', 'url' => 'https://tomsjavajive.com/'],
|
|
['name' => 'Shop', 'url' => 'https://tomsjavajive.com/shop.php'],
|
|
['name' => $product['name'], 'url' => $canonicalUrl],
|
|
];
|
|
|
|
require_once __DIR__ . '/includes/header.php';
|
|
?>
|
|
|
|
<section class="section" style="padding-top: 2rem;">
|
|
<div class="container">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: start;">
|
|
|
|
<!-- Product Images -->
|
|
<div>
|
|
<div class="product-main-image" style="border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 1rem;">
|
|
<img src="<?= htmlspecialchars($mainImage) ?>" alt="<?= htmlspecialchars($product['name']) ?>"
|
|
id="mainImage" style="width: 100%; aspect-ratio: 1; object-fit: cover;">
|
|
</div>
|
|
|
|
<?php if (count($images) > 1): ?>
|
|
<div style="display: flex; gap: 0.5rem; overflow-x: auto;">
|
|
<?php foreach ($images as $index => $img): ?>
|
|
<img src="<?= htmlspecialchars($img) ?>" alt="Product image <?= $index + 1 ?>"
|
|
onclick="document.getElementById('mainImage').src='<?= htmlspecialchars($img) ?>'"
|
|
style="width: 80px; height: 80px; object-fit: cover; border-radius: var(--radius-md); cursor: pointer; border: 2px solid transparent;"
|
|
class="thumbnail">
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Product Info -->
|
|
<div>
|
|
<?php if ($product['category']): ?>
|
|
<p class="text-muted mb-1">
|
|
<a href="/shop.php?category=<?= urlencode($product['category']) ?>">
|
|
<?= htmlspecialchars(ucfirst($product['category'])) ?>
|
|
</a>
|
|
</p>
|
|
<?php endif; ?>
|
|
|
|
<h1 style="margin-bottom: 0.5rem;"><?= htmlspecialchars($product['name']) ?></h1>
|
|
|
|
<!-- Rating -->
|
|
<?php if ($reviewCount > 0): ?>
|
|
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
|
|
<div class="stars" style="color: #F59E0B;">
|
|
<?php for ($i = 1; $i <= 5; $i++): ?>
|
|
<i class="fa<?= $i <= round($avgRating) ? 's' : 'r' ?> fa-star"></i>
|
|
<?php endfor; ?>
|
|
</div>
|
|
<span class="text-muted">(<?= $reviewCount ?> reviews)</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Price -->
|
|
<div class="product-price" style="font-size: 1.5rem; margin-bottom: 1.5rem;">
|
|
<?php if ($salePrice && $salePrice < $price): ?>
|
|
<span class="current"><?= formatCurrency($salePrice) ?></span>
|
|
<span class="original"><?= formatCurrency($price) ?></span>
|
|
<span style="background: var(--color-error); color: white; padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.875rem; margin-left: 0.5rem;">
|
|
Save <?= round((($price - $salePrice) / $price) * 100) ?>%
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="current"><?= formatCurrency($price) ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Stock Status -->
|
|
<p style="margin-bottom: 1rem;">
|
|
<?php if ($inStock): ?>
|
|
<span style="color: var(--color-success);"><i class="fas fa-check-circle"></i> In Stock</span>
|
|
<?php if ($product['stock'] <= 10): ?>
|
|
<span class="text-muted"> - Only <?= $product['stock'] ?> left!</span>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<span style="color: var(--color-error);"><i class="fas fa-times-circle"></i> Out of Stock</span>
|
|
<?php endif; ?>
|
|
</p>
|
|
|
|
<!-- Add to Cart Form -->
|
|
<?php if ($inStock): ?>
|
|
<form id="add-to-cart-form" style="margin-bottom: 2rem;">
|
|
<div style="display: flex; gap: 1rem; align-items: center; margin-bottom: 1rem;">
|
|
<div class="quantity-selector" style="display: flex; align-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-md);">
|
|
<button type="button" class="qty-minus btn" style="padding: 0.5rem 1rem;">-</button>
|
|
<input type="number" class="qty-input" value="1" min="1" max="<?= $product['stock'] ?>"
|
|
style="width: 60px; text-align: center; border: none; outline: none;">
|
|
<button type="button" class="qty-plus btn" style="padding: 0.5rem 1rem;">+</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-primary btn-lg add-to-cart-btn"
|
|
data-product-id="<?= $product['product_id'] ?>"
|
|
onclick="addToCart('<?= $product['product_id'] ?>', document.querySelector('.qty-input').value)">
|
|
<i class="fas fa-shopping-bag"></i> Add to Cart
|
|
</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<button class="btn btn-secondary btn-lg" disabled style="margin-bottom: 2rem;">
|
|
Out of Stock
|
|
</button>
|
|
<?php endif; ?>
|
|
|
|
<!-- Description -->
|
|
<div style="margin-bottom: 2rem;">
|
|
<h3 style="margin-bottom: 0.5rem;">Description</h3>
|
|
<div class="text-muted">
|
|
<?= nl2br(htmlspecialchars($product['description'])) ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Details -->
|
|
<div style="background: var(--color-background); padding: 1rem; border-radius: var(--radius-md);">
|
|
<h4 style="margin-bottom: 0.5rem;">Product Details</h4>
|
|
<table style="width: 100%;">
|
|
<?php if ($product['sku']): ?>
|
|
<tr>
|
|
<td class="text-muted">SKU</td>
|
|
<td><?= htmlspecialchars($product['sku']) ?></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php if ($product['weight']): ?>
|
|
<tr>
|
|
<td class="text-muted">Weight</td>
|
|
<td><?= $product['weight'] ?> oz</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<tr>
|
|
<td class="text-muted">Category</td>
|
|
<td><?= htmlspecialchars(ucfirst($product['category'] ?? 'General')) ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reviews Section -->
|
|
<div style="margin-top: 3rem;">
|
|
<h2 style="margin-bottom: 1.5rem;">Customer Reviews</h2>
|
|
|
|
<?php if (!empty($reviews)): ?>
|
|
<div style="display: grid; gap: 1rem;">
|
|
<?php foreach ($reviews as $review): ?>
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<div>
|
|
<strong><?= htmlspecialchars($review['customer_name']) ?></strong>
|
|
<?php if ($review['is_verified_purchase']): ?>
|
|
<span style="color: var(--color-success); font-size: 0.875rem;">
|
|
<i class="fas fa-check-circle"></i> Verified Purchase
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<span class="text-muted"><?= formatDate($review['created_at']) ?></span>
|
|
</div>
|
|
<div class="stars" style="color: #F59E0B; margin-bottom: 0.5rem;">
|
|
<?php for ($i = 1; $i <= 5; $i++): ?>
|
|
<i class="fa<?= $i <= $review['rating'] ? 's' : 'r' ?> fa-star"></i>
|
|
<?php endfor; ?>
|
|
</div>
|
|
<?php if ($review['title']): ?>
|
|
<h4 style="margin-bottom: 0.25rem;"><?= htmlspecialchars($review['title']) ?></h4>
|
|
<?php endif; ?>
|
|
<p class="text-muted mb-0"><?= nl2br(htmlspecialchars($review['comment'])) ?></p>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<p class="text-muted">No reviews yet. Be the first to review this product!</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Related Products -->
|
|
<?php if (!empty($relatedProducts)): ?>
|
|
<div style="margin-top: 3rem;">
|
|
<h2 style="margin-bottom: 1.5rem;">Related Products</h2>
|
|
<div class="product-grid">
|
|
<?php foreach ($relatedProducts as $related):
|
|
$relImages = json_decode($related['images'] ?? '[]', true);
|
|
$relImageUrl = !empty($relImages) ? $relImages[0] : '/assets/images/placeholder-product.svg';
|
|
?>
|
|
<div class="product-card">
|
|
<a href="/product.php?id=<?= $related['product_id'] ?>">
|
|
<div class="product-image">
|
|
<img src="<?= htmlspecialchars($relImageUrl) ?>" alt="<?= htmlspecialchars($related['name']) ?>">
|
|
</div>
|
|
<div class="product-info">
|
|
<h3 class="product-name"><?= htmlspecialchars($related['name']) ?></h3>
|
|
<div class="product-price">
|
|
<span class="current"><?= formatCurrency($related['sale_price'] ?? $related['price']) ?></span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|