Update search-script.js
parent
0890e90aef
commit
2e898ce357
438
search-script.js
438
search-script.js
|
@ -1,264 +1,212 @@
|
|||
<!-- HTML for Search Icon and Search Bar -->
|
||||
<div class="header__icon--search">🔍 Click to search</div>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Define product type keywords and their standardized forms
|
||||
const productTypes = {
|
||||
'mug': ['mug', 'cup'],
|
||||
'sticker': ['sticker'],
|
||||
'shirt': ['shirt', 't-shirt', 'tee'],
|
||||
'hoodie': ['hoodie', 'jacket'],
|
||||
'blanket': ['blanket'],
|
||||
// Add more product types and synonyms as needed
|
||||
};
|
||||
|
||||
<div id="search-bar" style="display: none;">
|
||||
<input type="text" id="search-input" placeholder="Search products..." />
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
const MAX_RESULTS = 20; // Limit the number of search results displayed
|
||||
let searchTimeout; // Timeout for delayed search
|
||||
|
||||
<style>
|
||||
/* CSS for search bar styling */
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.search-result-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.search-result-info {
|
||||
flex: 1;
|
||||
}
|
||||
.search-result-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
#search-bar.active {
|
||||
display: block;
|
||||
}
|
||||
#search-bar {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.search-results {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.header__icon--search {
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Define product type keywords and their standardized forms
|
||||
const productTypes = {
|
||||
'mug': ['mug', 'cup'],
|
||||
'sticker': ['sticker'],
|
||||
'shirt': ['shirt', 't-shirt', 'tee'],
|
||||
'hoodie': ['hoodie', 'jacket'],
|
||||
'blanket': ['blanket'],
|
||||
// Add more product types and synonyms as needed
|
||||
};
|
||||
|
||||
const MAX_RESULTS = 20; // Limit the number of search results displayed
|
||||
let searchTimeout; // Timeout for delayed search
|
||||
|
||||
// Function to convert plurals to singular
|
||||
function normalizeKeyword(keyword) {
|
||||
if (keyword.endsWith('s')) {
|
||||
return keyword.slice(0, -1);
|
||||
}
|
||||
return keyword;
|
||||
// Function to convert plurals to singular
|
||||
function normalizeKeyword(keyword) {
|
||||
if (keyword.endsWith('s')) {
|
||||
return keyword.slice(0, -1);
|
||||
}
|
||||
return keyword;
|
||||
}
|
||||
|
||||
// Function to standardize synonyms to a common keyword
|
||||
function standardizeKeyword(keyword) {
|
||||
const normalizedKeyword = normalizeKeyword(keyword);
|
||||
for (let standard in productTypes) {
|
||||
if (productTypes[standard].includes(normalizedKeyword)) {
|
||||
return standard;
|
||||
// Function to standardize synonyms to a common keyword
|
||||
function standardizeKeyword(keyword) {
|
||||
const normalizedKeyword = normalizeKeyword(keyword);
|
||||
for (let standard in productTypes) {
|
||||
if (productTypes[standard].includes(normalizedKeyword)) {
|
||||
return standard;
|
||||
}
|
||||
}
|
||||
return normalizedKeyword;
|
||||
}
|
||||
|
||||
// Function to extract keywords from the description
|
||||
function extractKeywords(description) {
|
||||
const keywordPrefix = "keywords: ";
|
||||
const keywordStart = description.toLowerCase().indexOf(keywordPrefix);
|
||||
if (keywordStart !== -1) {
|
||||
return description.slice(keywordStart + keywordPrefix.length).trim().toLowerCase();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fetch and parse the RSS feed
|
||||
fetch('https://merch.ookamikun.tv/.well-known/merchant-center/rss.xml')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(data, "application/xml");
|
||||
const items = xml.getElementsByTagName("item");
|
||||
const products = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
const idElement = item.getElementsByTagName("g:id")[0];
|
||||
const titleElement = item.getElementsByTagName("g:title")[0];
|
||||
const descriptionElement = item.getElementsByTagName("g:description")[0];
|
||||
const linkElement = item.getElementsByTagName("g:link")[0];
|
||||
const imageElement = item.getElementsByTagName("g:image_link")[0];
|
||||
const priceElement = item.getElementsByTagName("g:price")[0];
|
||||
const sizeElement = item.getElementsByTagName("g:size")[0];
|
||||
const colorElement = item.getElementsByTagName("g:color")[0];
|
||||
|
||||
if (idElement && titleElement && descriptionElement && linkElement && imageElement && priceElement) {
|
||||
const id = idElement.textContent || '';
|
||||
const title = titleElement.textContent.trim() || 'Untitled Product';
|
||||
const description = descriptionElement.textContent.trim() || 'No description available.';
|
||||
const link = linkElement.textContent || '#';
|
||||
const image = imageElement.textContent || 'default-image-url.jpg';
|
||||
const price = parseFloat(priceElement.textContent.replace(/[^\d.]/g, '')) || 0;
|
||||
const size = sizeElement ? sizeElement.textContent : 'One Size';
|
||||
const color = colorElement ? colorElement.textContent.trim().toLowerCase() : '';
|
||||
const keywords = extractKeywords(description); // Extracting keywords using the new function
|
||||
|
||||
if (!products[title]) {
|
||||
products[title] = {
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
image,
|
||||
prices: [],
|
||||
sizes: new Set(),
|
||||
color,
|
||||
keywords,
|
||||
searchText: title.toLowerCase() + ' ' + description.toLowerCase() + ' ' + color + ' ' + keywords,
|
||||
};
|
||||
}
|
||||
products[title].prices.push(price);
|
||||
if (size) products[title].sizes.add(size);
|
||||
}
|
||||
}
|
||||
return normalizedKeyword;
|
||||
}
|
||||
|
||||
// Function to extract keywords from the description
|
||||
function extractKeywords(description) {
|
||||
const keywordPrefix = "keywords: ";
|
||||
const keywordStart = description.toLowerCase().indexOf(keywordPrefix);
|
||||
if (keywordStart !== -1) {
|
||||
return description.slice(keywordStart + keywordPrefix.length).trim().toLowerCase();
|
||||
function calculateRelevanceScore(product, keywords, query) {
|
||||
let score = 0;
|
||||
let matchesAnyKeyword = false;
|
||||
|
||||
// Standardize and normalize keywords
|
||||
const standardizedKeywords = keywords.map(standardizeKeyword);
|
||||
|
||||
// Keywords match - highest weight (75%)
|
||||
const keywordMatches = standardizedKeywords.filter(keyword => product.keywords.includes(keyword));
|
||||
if (keywordMatches.length > 0) {
|
||||
score += keywordMatches.length * 75; // 75% weight for keywords
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Exact match - significant weight
|
||||
if (standardizedKeywords.includes(standardizeKeyword(product.title.toLowerCase())) ||
|
||||
standardizedKeywords.includes(standardizeKeyword(product.description.toLowerCase()))) {
|
||||
score += 30;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Product type match - significant weight
|
||||
let productTypeMatched = false;
|
||||
for (let type in productTypes) {
|
||||
const standardType = standardizeKeyword(type);
|
||||
if (standardizedKeywords.includes(standardType)) {
|
||||
score += 20;
|
||||
matchesAnyKeyword = true;
|
||||
productTypeMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!productTypeMatched && standardizedKeywords.some(keyword => productTypes.hasOwnProperty(keyword))) {
|
||||
score -= 20; // Stronger penalty for missing product type match if the user provided a product type keyword
|
||||
}
|
||||
|
||||
// Color match - medium weight
|
||||
const colorMatches = standardizedKeywords.filter(keyword => product.color.includes(keyword));
|
||||
if (colorMatches.length > 0) {
|
||||
score += colorMatches.length * 10;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Title match - lower weight
|
||||
const titleMatches = standardizedKeywords.filter(keyword => product.title.toLowerCase().includes(keyword));
|
||||
if (titleMatches.length > 0) {
|
||||
score += titleMatches.length * 5;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Description match - lower weight
|
||||
const descriptionMatches = standardizedKeywords.filter(keyword => product.description.toLowerCase().includes(keyword));
|
||||
if (descriptionMatches.length > 0) {
|
||||
score += descriptionMatches.length * 3;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Ensure the product is only considered if it contains at least one of the keywords
|
||||
if (!matchesAnyKeyword) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fetch and parse the RSS feed
|
||||
fetch('https://merch.ookamikun.tv/.well-known/merchant-center/rss.xml')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(data, "application/xml");
|
||||
const items = xml.getElementsByTagName("item");
|
||||
const products = {};
|
||||
const searchInput = document.querySelector('#search-input');
|
||||
const resultsContainer = document.querySelector('#search-results');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const query = this.value.toLowerCase().trim();
|
||||
const keywords = query.split(/\s+/);
|
||||
const results = [];
|
||||
|
||||
const idElement = item.getElementsByTagName("g:id")[0];
|
||||
const titleElement = item.getElementsByTagName("g:title")[0];
|
||||
const descriptionElement = item.getElementsByTagName("g:description")[0];
|
||||
const linkElement = item.getElementsByTagName("g:link")[0];
|
||||
const imageElement = item.getElementsByTagName("g:image_link")[0];
|
||||
const priceElement = item.getElementsByTagName("g:price")[0];
|
||||
const sizeElement = item.getElementsByTagName("g:size")[0];
|
||||
const colorElement = item.getElementsByTagName("g:color")[0];
|
||||
|
||||
if (idElement && titleElement && descriptionElement && linkElement && imageElement && priceElement) {
|
||||
const id = idElement.textContent || '';
|
||||
const title = titleElement.textContent.trim() || 'Untitled Product';
|
||||
const description = descriptionElement.textContent.trim() || 'No description available.';
|
||||
const link = linkElement.textContent || '#';
|
||||
const image = imageElement.textContent || 'default-image-url.jpg';
|
||||
const price = parseFloat(priceElement.textContent.replace(/[^\d.]/g, '')) || 0;
|
||||
const size = sizeElement ? sizeElement.textContent : 'One Size';
|
||||
const color = colorElement ? colorElement.textContent.trim().toLowerCase() : '';
|
||||
const keywords = extractKeywords(description); // Extracting keywords using the new function
|
||||
|
||||
if (!products[title]) {
|
||||
products[title] = {
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
image,
|
||||
prices: [],
|
||||
sizes: new Set(),
|
||||
color,
|
||||
keywords,
|
||||
searchText: title.toLowerCase() + ' ' + description.toLowerCase() + ' ' + color + ' ' + keywords,
|
||||
};
|
||||
}
|
||||
products[title].prices.push(price);
|
||||
if (size) products[title].sizes.add(size);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateRelevanceScore(product, keywords, query) {
|
||||
let score = 0;
|
||||
let matchesAnyKeyword = false;
|
||||
|
||||
// Standardize and normalize keywords
|
||||
const standardizedKeywords = keywords.map(standardizeKeyword);
|
||||
|
||||
// Keywords match - highest weight (75%)
|
||||
const keywordMatches = standardizedKeywords.filter(keyword => product.keywords.includes(keyword));
|
||||
if (keywordMatches.length > 0) {
|
||||
score += keywordMatches.length * 75; // 75% weight for keywords
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Exact match - significant weight
|
||||
if (standardizedKeywords.includes(standardizeKeyword(product.title.toLowerCase())) ||
|
||||
standardizedKeywords.includes(standardizeKeyword(product.description.toLowerCase()))) {
|
||||
score += 30;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Product type match - significant weight
|
||||
let productTypeMatched = false;
|
||||
for (let type in productTypes) {
|
||||
const standardType = standardizeKeyword(type);
|
||||
if (standardizedKeywords.includes(standardType)) {
|
||||
score += 20;
|
||||
matchesAnyKeyword = true;
|
||||
productTypeMatched = true;
|
||||
break;
|
||||
for (let key in products) {
|
||||
const product = products[key];
|
||||
const relevanceScore = calculateRelevanceScore(product, keywords, query);
|
||||
if (relevanceScore > 0) {
|
||||
results.push({ product, relevanceScore });
|
||||
}
|
||||
}
|
||||
if (!productTypeMatched && standardizedKeywords.some(keyword => productTypes.hasOwnProperty(keyword))) {
|
||||
score -= 20; // Stronger penalty for missing product type match if the user provided a product type keyword
|
||||
}
|
||||
|
||||
// Color match - medium weight
|
||||
const colorMatches = standardizedKeywords.filter(keyword => product.color.includes(keyword));
|
||||
if (colorMatches.length > 0) {
|
||||
score += colorMatches.length * 10;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
||||
|
||||
// Title match - lower weight
|
||||
const titleMatches = standardizedKeywords.filter(keyword => product.title.toLowerCase().includes(keyword));
|
||||
if (titleMatches.length > 0) {
|
||||
score += titleMatches.length * 5;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
// Render the top X results
|
||||
resultsContainer.innerHTML = '';
|
||||
results.slice(0, MAX_RESULTS).forEach(({ product }) => {
|
||||
const minPrice = Math.min(...product.prices).toFixed(2);
|
||||
const maxPrice = Math.max(...product.prices).toFixed(2);
|
||||
const priceRange = minPrice === maxPrice ? `$${minPrice}` : `$${minPrice} - $${maxPrice}`;
|
||||
|
||||
// Description match - lower weight
|
||||
const descriptionMatches = standardizedKeywords.filter(keyword => product.description.toLowerCase().includes(keyword));
|
||||
if (descriptionMatches.length > 0) {
|
||||
score += descriptionMatches.length * 3;
|
||||
matchesAnyKeyword = true;
|
||||
}
|
||||
|
||||
// Ensure the product is only considered if it contains at least one of the keywords
|
||||
if (!matchesAnyKeyword) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
const searchInput = document.querySelector('#search-input');
|
||||
const resultsContainer = document.querySelector('#search-results');
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
const query = this.value.toLowerCase().trim();
|
||||
const keywords = query.split(/\s+/);
|
||||
const results = [];
|
||||
|
||||
for (let key in products) {
|
||||
const product = products[key];
|
||||
const relevanceScore = calculateRelevanceScore(product, keywords, query);
|
||||
if (relevanceScore > 0) {
|
||||
results.push({ product, relevanceScore });
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
||||
|
||||
// Render the top X results
|
||||
resultsContainer.innerHTML = '';
|
||||
results.slice(0, MAX_RESULTS).forEach(({ product }) => {
|
||||
const minPrice = Math.min(...product.prices).toFixed(2);
|
||||
const maxPrice = Math.max(...product.prices).toFixed(2);
|
||||
const priceRange = minPrice === maxPrice ? `$${minPrice}` : `$${minPrice} - $${maxPrice}`;
|
||||
|
||||
const productElement = document.createElement('div');
|
||||
productElement.className = 'search-result-item';
|
||||
productElement.innerHTML = `
|
||||
<img src="${product.image}" alt="${product.title}" class="search-result-image">
|
||||
<div class="search-result-info">
|
||||
<h3>${product.title}</h3>
|
||||
<p>${product.description.substring(0, 100)}...</p>
|
||||
<div class="search-result-meta">
|
||||
<span>Sizes: ${[...product.sizes].join(', ')}</span>
|
||||
<span>Price: ${priceRange}</span>
|
||||
</div>
|
||||
const productElement = document.createElement('div');
|
||||
productElement.className = 'search-result-item';
|
||||
productElement.innerHTML = `
|
||||
<img src="${product.image}" alt="${product.title}" class="search-result-image">
|
||||
<div class="search-result-info">
|
||||
<h3>${product.title}</h3>
|
||||
<p>${product.description.substring(0, 100)}...</p>
|
||||
<div class="search-result-meta">
|
||||
<span>Sizes: ${[...product.sizes].join(', ')}</span>
|
||||
<span>Price: ${priceRange}</span>
|
||||
</div>
|
||||
`;
|
||||
resultsContainer.appendChild(productElement);
|
||||
});
|
||||
}, 500); // 0.5-second delay before performing the search
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error fetching or parsing the RSS feed:', error));
|
||||
</div>
|
||||
`;
|
||||
resultsContainer.appendChild(productElement);
|
||||
});
|
||||
}, 500); // 0.5-second delay before performing the search
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error fetching or parsing the RSS feed:', error));
|
||||
|
||||
const searchIcon = document.querySelector('.header__icon--search');
|
||||
const searchBar = document.querySelector('#search-bar');
|
||||
searchIcon.addEventListener('click', function() {
|
||||
searchBar.classList.toggle('active');
|
||||
searchBar.querySelector('input').focus();
|
||||
});
|
||||
const searchIcon = document.querySelector('.header__icon--search');
|
||||
const searchBar = document.querySelector('#search-bar');
|
||||
searchIcon.addEventListener('click', function() {
|
||||
searchBar.classList.toggle('active');
|
||||
searchBar.querySelector('input').focus();
|
||||
});
|
||||
</script>
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue