Files
tomsjavajive/product.php
T
myron 15bcef262f Google Merchant Center trust signal improvements
- 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>
2026-06-15 18:46:21 +00:00

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'; ?>