rajkhanke's picture
Upload 3 files
bbd7d3c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pizza Recommendation System</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--primary: #FF6B35;
--primary-dark: #E85D2C;
--secondary: #FFF3E0;
--text-dark: #333333;
--text-light: #FFFFFF;
}
body {
font-family: 'Poppins', sans-serif;
background-color: #FFF8F0;
color: var(--text-dark);
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23FF6B35' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.header { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); box-shadow: 0 4px 15px rgba(255, 107, 53, 0.2); }
.logo { font-weight: 700; font-size: 1.8rem; color: var(--text-light); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); }
.logo i { margin-right: 8px; }
.card { background-color: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; cursor: pointer; }
.card:hover { transform: translateY(-8px); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
.card-image { height: 180px; overflow: hidden; position: relative; background-color: #f0f0f0; }
.card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; }
.card:hover .card-image img { transform: scale(1.05); }
.price-tag { position: absolute; top: 12px; right: 12px; background-color: var(--primary); color: var(--text-light); padding: 4px 10px; border-radius: 20px; font-weight: 600; font-size: 0.9rem; box-shadow: 0 4px 6px rgba(255, 107, 53, 0.25); }
.card-title { font-size: 1.2rem; font-weight: 600; color: var(--primary-dark); }
.rating { display: flex; align-items: center; }
.rating i { color: #FFB800; margin-right: 4px; }
.filter-section { background-color: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
.filter-header { background-color: var(--primary); color: var(--text-light); padding: 15px 20px; border-top-left-radius: 12px; border-top-right-radius: 12px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.form-label { font-weight: 500; margin-bottom: 6px; color: var(--text-dark); display: block; }
.btn-primary { background-color: var(--primary); color: var(--text-light); padding: 10px 20px; border-radius: 8px; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 6px rgba(255, 107, 53, 0.25); }
.btn-primary:hover { background-color: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 6px 8px rgba(255, 107, 53, 0.3); }
.btn-outline { border: 2px solid var(--primary); color: var(--primary); padding: 8px 18px; border-radius: 8px; font-weight: 600; transition: all 0.3s ease; }
.btn-outline:hover { background-color: var(--primary); color: var(--text-light); }
input[type="range"].range-slider { width: 100%; -webkit-appearance: none; appearance: none; height: 8px; background: #ddd; border-radius: 5px; outline: none; opacity: 0.7; transition: opacity .2s; cursor: pointer;}
input[type="range"].range-slider:hover { opacity: 1;}
input[type="range"].range-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; background: var(--primary); border-radius: 50%; cursor: pointer; }
input[type="range"].range-slider::-moz-range-thumb { width: 20px; height: 20px; background: var(--primary); border-radius: 50%; cursor: pointer; border: none; }
.multiselect-dropdown { position: relative; }
.multiselect-dropdown .selected-input { border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 12px; width: 100%; background-color: #f8fafc; transition: all 0.3s ease; }
.multiselect-dropdown .selected-input:focus-within, .multiselect-dropdown .selected-input.active { border-color: var(--primary); outline: none; box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.2); }
.multiselect-dropdown .dropdown-container { display: none; position: absolute; top: 100%; left: 0; width: 100%; max-height: 200px; overflow-y: auto; background-color: white; border: 1px solid #e2e8f0; border-radius: 0 0 8px 8px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 10; padding: 8px; margin-top: -1px; }
.multiselect-dropdown .dropdown-container.active { display: block; }
.multiselect-dropdown .option-item, .topping-item { padding: 6px 10px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; transition: all 0.2s ease; }
.multiselect-dropdown .option-item:hover, .topping-item:hover { background-color: var(--secondary); }
.multiselect-dropdown .option-item.selected, .topping-item.selected { background-color: var(--primary); color: var(--text-light); }
.selected-options-display, .selected-toppings { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.selected-tag, .selected-topping { background-color: var(--secondary); color: var(--primary-dark); padding: 4px 10px; border-radius: 20px; font-size: 0.8rem; display: flex; align-items: center; gap: 6px; }
.selected-tag i, .selected-topping i { cursor: pointer; }
.range-values { display: flex; justify-content: space-between; margin-top: 8px; font-size: 0.9rem; color: #64748b; }
.toggle-filters { display: none; }
.pizza-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; margin-right: 6px; margin-bottom: 6px; }
.badge-veg { background-color: #DCFCE7; color: #16A34A; }
.badge-non-veg { background-color: #FEE2E2; color: #DC2626; }
.badge-vegan { background-color: #E0F2FE; color: #0284C7; }
.badge-spice { background-color: #FEF3C7; color: #D97706; }
.loader { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
@media (max-width: 1023px) { .toggle-filters { display: block; background: none; border: none; color: white; font-size: 1.2rem; } .filters-container { max-height: 0; overflow: hidden; transition: max-height 0.5s ease-in-out, padding 0.5s ease-in-out; padding: 0 1.5rem; } .filters-container.active { max-height: 2000px; padding: 1.5rem; } }
.modal-item strong { color: var(--primary-dark); }
</style>
</head>
<body>
<header class="header py-4 px-6 mb-8">
<div class="container mx-auto flex items-center justify-between">
<div class="flex items-center">
<span class="logo"><i class="fas fa-pizza-slice"></i>Pizza Recommendation System</span>
</div>
<div class="hidden md:block">
<p class="text-white text-sm">Find your perfect pizza match!</p>
</div>
</div>
</header>
<div class="container mx-auto px-4 mb-10">
<div class="flex flex-wrap -mx-4">
<div class="w-full lg:w-1/4 px-4 mb-8 lg:mb-0">
<div class="filter-section">
<div class="filter-header">
<h2 class="text-lg">Customize Your Pizza</h2>
<button class="toggle-filters lg:hidden"><i class="fas fa-sliders-h"></i></button>
</div>
<div class="filters-container p-6">
<form id="pizza-filters">
<!-- Toppings Selection -->
<div class="mb-6">
<label class="form-label">Toppings</label>
<div class="multiselect-dropdown" data-filter-key="toppings">
<div class="selected-input p-2 rounded-lg cursor-pointer flex items-center justify-between">
<span>Select toppings</span><i class="fas fa-chevron-down text-sm"></i>
</div>
<div class="dropdown-container">
<input type="text" class="w-full mb-2 p-2 border border-gray-200 rounded text-sm" placeholder="Search toppings...">
<div class="options-list toppings-list">
{% for topping in toppings %}<div class="option-item topping-item text-sm" data-value="{{ topping }}">{{ topping }}</div>{% endfor %}
</div>
</div>
<div class="selected-options-display selected-toppings mt-2"></div>
</div>
</div>
<div class="mb-6">
<label for="price-range" class="form-label">Max Price (₹)</label>
<input type="range" id="price-range" name="price_range_max" min="199" max="1999" step="50" class="range-slider" value="1999">
<div class="range-values"><span>₹199</span><span id="price-value">₹1999</span></div>
</div>
<div class="mb-6">
<label for="slices" class="form-label">Min. Number of Slices</label>
<input type="range" id="slices" name="slices" min="4" max="12" step="1" class="range-slider" value="4">
<div class="range-values"><span>4</span><span id="slices-value">4</span><span>12</span></div>
</div>
<div class="mb-6">
<label for="rating" class="form-label">Minimum Rating</label>
<input type="range" id="rating" name="rating" min="0" max="5" step="0.5" class="range-slider" value="0">
<div class="flex justify-between items-center mt-2"><div class="flex items-center" id="rating-display"></div></div>
</div>
<div class="mb-6">
<label for="prep-time" class="form-label">Max Preparation Time (min)</label>
<input type="range" id="prep-time" name="prep_time" min="12" max="90" step="1" class="range-slider" value="90">
<div class="range-values"><span>12 min</span><span id="prep-time-value">90 min</span><span>90 min</span></div>
</div>
<!-- Categorical Filters - Transformed to Multi-Select -->
{% set filter_map = {
'servingsize': {'label': 'Serving Size', 'options': filter_options.servingsize},
'dietarycategory': {'label': 'Dietary Category', 'options': filter_options.dietarycategory},
'spicelevel': {'label': 'Spice Level', 'options': filter_options.spicelevel},
'crusttype': {'label': 'Crust Type', 'options': filter_options.crusttype},
'populargroup': {'label': 'Popular Among', 'options': filter_options.populargroup},
'saucetype': {'label': 'Sauce Type', 'options': filter_options.saucetype},
'cheeseamount': {'label': 'Cheese Amount', 'options': filter_options.cheeseamount},
'restaurantchain': {'label': 'Restaurant Chain', 'options': filter_options.restaurantchain},
'seasonalavailability': {'label': 'Seasonal Availability', 'options': filter_options.seasonalavailability},
'breadtype': {'label': 'Bread Type', 'options': filter_options.breadtype}
} %}
{% for key, item in filter_map.items() %}
{% if item.options %}
<div class="mb-6">
<label class="form-label">{{ item.label }}</label>
<div class="multiselect-dropdown" data-filter-key="{{ key }}">
<div class="selected-input p-2 rounded-lg cursor-pointer flex items-center justify-between">
<span>Select {{ item.label.lower() }}(s)</span>
<i class="fas fa-chevron-down text-sm"></i>
</div>
<div class="dropdown-container">
<div class="options-list">
{% for option_val in item.options %}
<div class="option-item text-sm" data-value="{{ option_val }}">{{ option_val }}</div>
{% endfor %}
</div>
</div>
<div class="selected-options-display mt-2"></div>
</div>
</div>
{% endif %}
{% endfor %}
<div class="flex justify-between mt-8">
<button type="submit" class="btn-primary text-sm"><i class="fas fa-search mr-2"></i>Find Pizza</button>
<button type="reset" class="btn-outline text-sm"><i class="fas fa-redo mr-1"></i>Reset</button>
</div>
</form>
</div>
</div>
</div>
<div class="w-full lg:w-3/4 px-4">
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-2xl font-bold mb-1 text-gray-800">Recommended Pizzas</h2>
<p class="text-gray-600 mb-4 text-sm" id="recommendation-subtitle">Discover pizzas tailored to your taste!</p>
<div id="loading" class="hidden py-6"><div class="loader"></div><p class="text-center mt-4 text-gray-600 text-sm">Finding your perfect pizza match...</p></div>
<div id="pizza-recommendations" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</div>
</div>
</div>
</div>
<!-- Pizza Detail Modal (structure remains mostly the same) -->
<div id="pizza-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center p-4 z-50 transition-opacity duration-300 ease-in-out opacity-0">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto transform scale-95 transition-transform duration-300 ease-in-out">
<div class="flex justify-between items-center mb-4 border-b pb-3">
<h2 id="modal-pizza-name" class="text-2xl font-bold text-primary">Pizza Name</h2>
<button id="modal-close-btn" class="text-gray-500 hover:text-gray-800 text-3xl leading-none">×</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div class="md:pr-4">
<img id="modal-pizza-image" src="{{ default_image_url }}" alt="Pizza Image" class="w-full h-64 object-cover rounded-lg mb-4 shadow" onerror="this.onerror=null;this.src='{{ default_image_url }}';">
<div class="flex justify-between items-center mb-2">
<p class="text-3xl font-semibold text-primary-dark"><span id="modal-pizza-price"></span></p>
<div class="flex items-center">
<div id="modal-pizza-rating-stars" class="rating mr-2 text-xl"></div>
<span id="modal-pizza-rating-text" class="text-sm text-gray-600"></span>
</div>
</div>
</div>
<div class="space-y-2 text-sm md:pl-4 border-t md:border-t-0 md:border-l pt-4 md:pt-0">
<p class="modal-item"><strong>Description:</strong><br><span id="modal-pizza-description" class="text-gray-700 block mt-1"></span></p>
<p class="modal-item"><strong>Toppings:</strong><br><span id="modal-pizza-toppings" class="text-gray-700 block mt-1"></span></p>
<p class="modal-item"><strong>Slices:</strong> <span id="modal-pizza-slices" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Serving Size:</strong> <span id="modal-pizza-serving-size" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Dietary Category:</strong> <span id="modal-pizza-dietary" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Spice Level:</strong> <span id="modal-pizza-spice" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Crust Type:</strong> <span id="modal-pizza-crust-type" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Sauce Type:</strong> <span id="modal-pizza-sauce" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Cheese Amount:</strong> <span id="modal-pizza-cheese" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Calories per Slice:</strong> <span id="modal-pizza-calories" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Preparation Time:</strong> <span id="modal-pizza-prep-time" class="text-gray-700"></span> min</p>
<p class="modal-item"><strong>Restaurant:</strong> <span id="modal-pizza-restaurant" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Popular Group:</strong> <span id="modal-pizza-popular-group" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Seasonal Availability:</strong> <span id="modal-pizza-seasonal" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Bread Type:</strong> <span id="modal-pizza-bread" class="text-gray-700"></span></p>
<p class="modal-item"><strong>Allergens:</strong><br><span id="modal-pizza-allergens" class="text-gray-700 block mt-1"></span></p>
</div>
</div>
</div>
</div>
<script>
const DEFAULT_IMAGE_URL_JS = "{{ default_image_url }}";
let allPizzasData = {{ default_recommendations | tojson }};
const allFilterOptions = {{ filter_options | tojson }}; // Get all filter options from Flask
document.addEventListener('DOMContentLoaded', function() {
// --- Range Sliders ---
const priceRange = document.getElementById('price-range');
const priceValue = document.getElementById('price-value');
if(priceRange && priceValue) { priceRange.addEventListener('input', () => priceValue.textContent = `₹${priceRange.value}`); priceValue.textContent = `₹${priceRange.value}`; }
const slicesRange = document.getElementById('slices');
const slicesValue = document.getElementById('slices-value');
if(slicesRange && slicesValue) { slicesRange.addEventListener('input', () => slicesValue.textContent = slicesRange.value); slicesValue.textContent = slicesRange.value; }
const prepTimeRange = document.getElementById('prep-time');
const prepTimeValue = document.getElementById('prep-time-value');
if(prepTimeRange && prepTimeValue) { prepTimeRange.addEventListener('input', () => prepTimeValue.textContent = `${prepTimeRange.value} min`); prepTimeValue.textContent = `${prepTimeRange.value} min`; }
const ratingRange = document.getElementById('rating');
const ratingDisplay = document.getElementById('rating-display');
if(ratingRange && ratingDisplay) { ratingRange.addEventListener('input', () => updateRatingStars(ratingRange.value, ratingDisplay)); updateRatingStars(ratingRange.value, ratingDisplay); }
function updateRatingStars(rating, displayElement, isModal = false) {
if (!displayElement) return;
let starsHTML = ''; const r = parseFloat(rating);
for (let i = 0; i < 5; i++) {
if (i < Math.floor(r)) starsHTML += '<i class="fas fa-star text-yellow-400"></i>';
else if (i < r) starsHTML += '<i class="fas fa-star-half-alt text-yellow-400"></i>';
else starsHTML += '<i class="far fa-star text-yellow-400"></i>';
}
if (isModal) displayElement.innerHTML = starsHTML;
else displayElement.innerHTML = starsHTML + `<span class="ml-2 text-sm text-gray-600">(${r.toFixed(1)})</span>`;
}
// --- Mobile Filters Toggle ---
const toggleFiltersBtn = document.querySelector('.toggle-filters');
const filtersContainerEl = document.querySelector('.filters-container');
if (toggleFiltersBtn && filtersContainerEl) {
toggleFiltersBtn.addEventListener('click', () => {
filtersContainerEl.classList.toggle('active');
toggleFiltersBtn.querySelector('i').classList.toggle('fa-sliders-h');
toggleFiltersBtn.querySelector('i').classList.toggle('fa-times');
});
}
// --- Multi-Select Dropdown Logic ---
let selectedCategoricalFilters = {}; // Stores { filterKey: [val1, val2], ... }
const multiSelectDropdowns = document.querySelectorAll('.multiselect-dropdown');
multiSelectDropdowns.forEach(dropdownEl => {
const filterKey = dropdownEl.dataset.filterKey;
selectedCategoricalFilters[filterKey] = []; // Initialize
const selectInput = dropdownEl.querySelector('.selected-input');
const dropdownContainer = dropdownEl.querySelector('.dropdown-container');
const optionsList = dropdownEl.querySelector('.options-list');
const searchInput = dropdownEl.querySelector('input[type="text"]'); // For toppings search
selectInput.addEventListener('click', () => {
dropdownContainer.classList.toggle('active');
selectInput.classList.toggle('active');
});
optionsList.querySelectorAll('.option-item').forEach(item => {
item.addEventListener('click', function() {
const value = this.dataset.value;
const currentSelections = selectedCategoricalFilters[filterKey];
if (currentSelections.includes(value)) {
selectedCategoricalFilters[filterKey] = currentSelections.filter(v => v !== value);
this.classList.remove('selected');
} else {
currentSelections.push(value);
this.classList.add('selected');
}
updateSelectedTagsDisplay(dropdownEl, filterKey);
if (filterKey !== 'toppings') { // Auto-close for non-topping simple selects
// dropdownContainer.classList.remove('active');
// selectInput.classList.remove('active');
}
});
});
if (searchInput) { // Toppings search
searchInput.addEventListener('input', function() {
const searchValue = this.value.toLowerCase();
optionsList.querySelectorAll('.option-item').forEach(item => {
item.style.display = item.textContent.toLowerCase().includes(searchValue) ? '' : 'none';
});
});
}
});
// Close dropdowns if clicked outside
document.addEventListener('click', (e) => {
multiSelectDropdowns.forEach(dropdownEl => {
if (!dropdownEl.contains(e.target)) {
dropdownEl.querySelector('.dropdown-container').classList.remove('active');
dropdownEl.querySelector('.selected-input').classList.remove('active');
}
});
});
function updateSelectedTagsDisplay(dropdownEl, filterKey) {
const selectedDisplayContainer = dropdownEl.querySelector('.selected-options-display');
const selectInputTextSpan = dropdownEl.querySelector('.selected-input span');
const currentSelections = selectedCategoricalFilters[filterKey];
const filterConfig = Array.from(multiSelectDropdowns).find(el => el.dataset.filterKey === filterKey);
const label = filterConfig ? (filterConfig.closest('.mb-6').querySelector('.form-label')?.textContent || filterKey) : filterKey;
selectedDisplayContainer.innerHTML = '';
currentSelections.forEach(value => {
const tag = document.createElement('div');
tag.className = 'selected-tag'; // Generic class for tags
tag.innerHTML = `<span>${value}</span><i class="fas fa-times ml-1 text-xs" data-value="${value}"></i>`;
tag.querySelector('i').addEventListener('click', function() {
const valToRemove = this.dataset.value;
selectedCategoricalFilters[filterKey] = selectedCategoricalFilters[filterKey].filter(v => v !== valToRemove);
dropdownEl.querySelector(`.option-item[data-value="${valToRemove}"]`)?.classList.remove('selected');
updateSelectedTagsDisplay(dropdownEl, filterKey);
});
selectedDisplayContainer.appendChild(tag);
});
if (currentSelections.length > 0) {
selectInputTextSpan.textContent = `${currentSelections.length} ${label.toLowerCase()}(s) selected`;
} else {
selectInputTextSpan.textContent = `Select ${label.toLowerCase()}(s)`;
}
}
// --- Form Submission & Reset ---
const pizzaFiltersForm = document.getElementById('pizza-filters');
const pizzaRecommendationsEl = document.getElementById('pizza-recommendations');
const loadingIndicator = document.getElementById('loading');
const recommendationSubtitle = document.getElementById('recommendation-subtitle');
if(pizzaFiltersForm) {
pizzaFiltersForm.addEventListener('submit', function(e) { e.preventDefault(); fetchRecommendations(); });
pizzaFiltersForm.addEventListener('reset', function() {
// Reset range sliders
if(priceRange && priceValue) { priceRange.value = "1999"; priceValue.textContent = `₹1999`; }
if(slicesRange && slicesValue) { slicesRange.value = "4"; slicesValue.textContent = "4"; }
if(prepTimeRange && prepTimeValue) { prepTimeRange.value = "90"; prepTimeValue.textContent = `90 min`; }
if(ratingRange && ratingDisplay) { ratingRange.value = "0"; updateRatingStars("0", ratingDisplay); }
// Reset multi-selects
Object.keys(selectedCategoricalFilters).forEach(key => {
selectedCategoricalFilters[key] = [];
const dropdownEl = document.querySelector(`.multiselect-dropdown[data-filter-key="${key}"]`);
if (dropdownEl) {
dropdownEl.querySelectorAll('.option-item.selected').forEach(el => el.classList.remove('selected'));
updateSelectedTagsDisplay(dropdownEl, key);
}
});
if (document.querySelector('.multiselect-dropdown[data-filter-key="toppings"] input[type="text"]')) {
document.querySelector('.multiselect-dropdown[data-filter-key="toppings"] input[type="text"]').value = ""; // Clear search
document.querySelectorAll('.multiselect-dropdown[data-filter-key="toppings"] .option-item').forEach(item => item.style.display = ''); // Show all
}
setTimeout(() => displayRecommendations(allPizzasData, true), 100);
if(recommendationSubtitle) recommendationSubtitle.textContent = `Showing all ${allPizzasData.length} available pizzas. Customize your search above!`;
});
}
function fetchRecommendations() {
if(loadingIndicator) loadingIndicator.classList.remove('hidden');
if(pizzaRecommendationsEl) pizzaRecommendationsEl.classList.add('hidden');
if(recommendationSubtitle) recommendationSubtitle.textContent = "Based on your preferences, we think you'll love these pizzas!";
const preferences = {
// Numerical/Range preferences
price_range: priceRange ? [199, parseInt(priceRange.value)] : null,
slices: slicesRange ? parseInt(slicesRange.value) : null,
rating: ratingRange ? parseFloat(ratingRange.value) : null,
prep_time: prepTimeRange ? parseInt(prepTimeRange.value) : null,
// Categorical (multi-select) preferences from selectedCategoricalFilters
// Keys here must match what backend expects (e.g. 'servingsize', not 'serving_size')
toppings: selectedCategoricalFilters['toppings'] || [],
servingsize: selectedCategoricalFilters['servingsize'] || [],
dietarycategory: selectedCategoricalFilters['dietarycategory'] || [],
spicelevel: selectedCategoricalFilters['spicelevel'] || [],
crusttype: selectedCategoricalFilters['crusttype'] || [],
populargroup: selectedCategoricalFilters['populargroup'] || [],
saucetype: selectedCategoricalFilters['saucetype'] || [],
cheeseamount: selectedCategoricalFilters['cheeseamount'] || [],
restaurantchain: selectedCategoricalFilters['restaurantchain'] || [],
seasonalavailability: selectedCategoricalFilters['seasonalavailability'] || [],
breadtype: selectedCategoricalFilters['breadtype'] || []
};
const finalPreferences = {};
for (const key in preferences) {
if (preferences[key] !== null) { // Allow empty arrays (for "Any" categorical)
if (Array.isArray(preferences[key])) { // For multi-selects including toppings
finalPreferences[key] = preferences[key];
} else { // For single value numerical/range
finalPreferences[key] = preferences[key];
}
}
}
// console.log("Sending preferences:", finalPreferences);
fetch('/recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(finalPreferences),
})
.then(response => {
if (!response.ok) {
return response.json().then(errData => { throw { status: response.status, data: errData }; })
.catch(() => { throw { status: response.status, data: { error: "Server error, could not parse details." } }; });
}
return response.json();
})
.then(data => {
if(loadingIndicator) loadingIndicator.classList.add('hidden');
if(pizzaRecommendationsEl) pizzaRecommendationsEl.classList.remove('hidden');
displayRecommendations(data);
})
.catch(errorObj => {
console.error('Error fetching recommendations:', errorObj);
if(loadingIndicator) loadingIndicator.classList.add('hidden');
if(pizzaRecommendationsEl) pizzaRecommendationsEl.classList.remove('hidden');
let errorMsg = "We couldn't fetch recommendations. Please try again later.";
if (errorObj.data && errorObj.data.error) { errorMsg = errorObj.data.error; }
if(pizzaRecommendationsEl) pizzaRecommendationsEl.innerHTML = `
<div class="col-span-1 md:col-span-2 xl:col-span-3 text-center py-10">
<i class="fas fa-exclamation-triangle text-red-500 text-5xl mb-4"></i>
<h3 class="text-xl font-semibold mb-2">Oops! Something went wrong</h3>
<p class="text-gray-600">${errorMsg}</p>
</div>`;
});
}
function displayRecommendations(pizzas, isDefaultAll = false) {
if (!pizzaRecommendationsEl) return;
pizzaRecommendationsEl.innerHTML = '';
if (isDefaultAll) { allPizzasData = pizzas; } // Update global store if it's the initial full load
if (!pizzas || pizzas.length === 0) {
if(recommendationSubtitle) recommendationSubtitle.textContent = "No pizzas match your current criteria.";
pizzaRecommendationsEl.innerHTML = `
<div class="col-span-1 md:col-span-2 xl:col-span-3 text-center py-10">
<i class="fas fa-search-minus text-primary text-5xl mb-4"></i>
<h3 class="text-xl font-semibold mb-2">No Pizzas Found</h3>
<p class="text-gray-600">Try adjusting your filters for a wider search!</p>
</div>`;
return;
}
if(recommendationSubtitle && !isDefaultAll) recommendationSubtitle.textContent = `Found ${pizzas.length} pizza(s) matching your taste!`;
else if (recommendationSubtitle && isDefaultAll) recommendationSubtitle.textContent = `Showing all ${pizzas.length} available pizzas.`;
let html = '';
pizzas.forEach((pizza) => {
const p_rating = parseFloat(pizza.rating || 0);
let ratingStarsHTML = '';
for (let i = 0; i < 5; i++) {
if (i < Math.floor(p_rating)) ratingStarsHTML += '<i class="fas fa-star"></i>';
else if (i < p_rating) ratingStarsHTML += '<i class="fas fa-star-half-alt"></i>';
else ratingStarsHTML += '<i class="far fa-star"></i>';
}
const dietaryCategory = pizza.dietary_category;
let dietaryBadgeClass = 'badge-veg'; let dietaryText = dietaryCategory;
if (dietaryCategory === 'Non-Vegetarian') { dietaryBadgeClass = 'badge-non-veg'; dietaryText = 'Non-Veg'; }
else if (dietaryCategory === 'Vegan') { dietaryBadgeClass = 'badge-vegan'; }
const pizzaName = pizza.name || 'Unknown Pizza';
const price = pizza.price || 'N/A';
const ratingCount = pizza.rating_count || 0;
const spiceLevel = pizza.spice_level;
const description = pizza.description || 'No description available.';
const cheeseAmount = pizza.cheese_amount;
const slicesCount = pizza.slices || 'N/A';
const servingSize = pizza.serving_size;
const calories = pizza.calories;
const prepTime = pizza.prep_time;
const toppings = pizza.toppings;
const imageUrl = pizza.image_url || DEFAULT_IMAGE_URL_JS;
html += `
<div class="card" data-pizza-id="${pizza.id}">
<div class="card-image">
<img src="${imageUrl}" alt="${pizzaName}" onerror="this.onerror=null;this.src='${DEFAULT_IMAGE_URL_JS}';">
<div class="price-tag">₹${price}</div>
</div>
<div class="p-4">
<h3 class="card-title mb-2 truncate" title="${pizzaName}">${pizzaName}</h3>
<div class="flex items-center mb-3"><div class="rating mr-2">${ratingStarsHTML}</div><span class="text-xs text-gray-600">${p_rating.toFixed(1)} (${ratingCount})</span></div>
<div class="mb-3">
${dietaryCategory ? `<span class="pizza-badge ${dietaryBadgeClass}">${dietaryText}</span>` : ''}
${spiceLevel ? `<span class="pizza-badge badge-spice">${spiceLevel}</span>` : ''}
</div>
<p class="text-xs text-gray-600 mb-4 line-clamp-3 h-12">${description}</p>
<div class="text-xs text-gray-500 mb-3 space-y-1">
${cheeseAmount ? `<div><i class="fas fa-cheese mr-2 w-4 text-center"></i>${cheeseAmount} cheese</div>` : ''}
<div><i class="fas fa-utensils mr-2 w-4 text-center"></i>${slicesCount} slices (${servingSize || 'N/A'})</div>
${calories ? `<div><i class="fas fa-fire mr-2 w-4 text-center"></i>${calories} cal/slice</div>` : ''}
${prepTime ? `<div><i class="fas fa-clock mr-2 w-4 text-center"></i>${prepTime} mins</div>` : ''}
</div>
${toppings ? `<div class="border-t pt-3"><h4 class="text-xs font-semibold mb-1">Toppings:</h4><p class="text-xs text-gray-600 truncate">${toppings.replace(/;/g, ', ')}</p></div>` : ''}
</div>
</div>`;
});
pizzaRecommendationsEl.innerHTML = html;
pizzaRecommendationsEl.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', function() {
const pizzaId = parseInt(this.dataset.pizzaId);
// Try finding in current displayed list first, then fallback to allPizzasData
const clickedPizzaData = pizzas.find(p => p.id === pizzaId) || allPizzasData.find(p => p.id === pizzaId);
if (clickedPizzaData) openPizzaModal(clickedPizzaData);
else console.error("Could not find pizza data for ID:", pizzaId, "in current recommendations or allPizzasData");
});
});
}
// --- Modal Functionality ---
const pizzaModal = document.getElementById('pizza-modal');
const modalCloseBtn = document.getElementById('modal-close-btn');
function openPizzaModal(pizza) {
if (!pizzaModal || !pizza) return;
document.getElementById('modal-pizza-name').textContent = pizza.name || 'N/A';
document.getElementById('modal-pizza-image').src = pizza.image_url || DEFAULT_IMAGE_URL_JS;
document.getElementById('modal-pizza-image').onerror = function() { this.onerror=null; this.src=DEFAULT_IMAGE_URL_JS; };
document.getElementById('modal-pizza-price').textContent = pizza.price || 'N/A';
const modalRating = parseFloat(pizza.rating || 0);
updateRatingStars(modalRating, document.getElementById('modal-pizza-rating-stars'), true);
document.getElementById('modal-pizza-rating-text').textContent = `(${modalRating.toFixed(1)} from ${pizza.rating_count || 0} ratings)`;
document.getElementById('modal-pizza-description').textContent = pizza.description || 'N/A';
document.getElementById('modal-pizza-toppings').textContent = (pizza.toppings || 'N/A').replace(/;/g, ', ');
document.getElementById('modal-pizza-slices').textContent = pizza.slices || 'N/A';
document.getElementById('modal-pizza-serving-size').textContent = pizza.serving_size || 'N/A';
document.getElementById('modal-pizza-dietary').textContent = pizza.dietary_category || 'N/A';
document.getElementById('modal-pizza-spice').textContent = pizza.spice_level || 'N/A';
document.getElementById('modal-pizza-crust-type').textContent = pizza.crust_type || 'N/A';
document.getElementById('modal-pizza-sauce').textContent = pizza.sauce_type || 'N/A';
document.getElementById('modal-pizza-cheese').textContent = pizza.cheese_amount || 'N/A';
document.getElementById('modal-pizza-calories').textContent = pizza.calories || 'N/A';
document.getElementById('modal-pizza-prep-time').textContent = pizza.prep_time || 'N/A';
document.getElementById('modal-pizza-restaurant').textContent = pizza.restaurant || 'N/A';
document.getElementById('modal-pizza-popular-group').textContent = pizza.popular_group || 'N/A';
document.getElementById('modal-pizza-seasonal').textContent = pizza.seasonal || 'N/A';
document.getElementById('modal-pizza-bread').textContent = pizza.bread_type || 'N/A';
document.getElementById('modal-pizza-allergens').textContent = (pizza.allergens || 'N/A').replace(/;/g, ', ');
pizzaModal.classList.remove('hidden');
setTimeout(() => { pizzaModal.classList.remove('opacity-0'); pizzaModal.querySelector('.transform').classList.remove('scale-95'); }, 10);
document.body.style.overflow = 'hidden';
}
if(modalCloseBtn) modalCloseBtn.addEventListener('click', closeModal);
if(pizzaModal) pizzaModal.addEventListener('click', function(e) { if (e.target === pizzaModal) closeModal(); });
function closeModal() {
if (!pizzaModal) return;
pizzaModal.classList.add('opacity-0');
pizzaModal.querySelector('.transform').classList.add('scale-95');
setTimeout(() => pizzaModal.classList.add('hidden'), 300);
document.body.style.overflow = 'auto';
}
// --- Initial Page Load ---
displayRecommendations(allPizzasData, true); // Display all default pizzas
if(recommendationSubtitle) recommendationSubtitle.textContent = `Showing all ${allPizzasData.length} available pizzas. Customize your search above!`;
// Initialize display text for all multi-selects
multiSelectDropdowns.forEach(dropdownEl => {
const filterKey = dropdownEl.dataset.filterKey;
updateSelectedTagsDisplay(dropdownEl, filterKey);
});
});
</script>
</body>
</html>