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 -->
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
<div class="header__icon--search">🔍 Click to search</div>
|
// 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;">
|
const MAX_RESULTS = 20; // Limit the number of search results displayed
|
||||||
<input type="text" id="search-input" placeholder="Search products..." />
|
let searchTimeout; // Timeout for delayed search
|
||||||
<div id="search-results" class="search-results"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
// Function to convert plurals to singular
|
||||||
/* CSS for search bar styling */
|
function normalizeKeyword(keyword) {
|
||||||
.search-result-item {
|
if (keyword.endsWith('s')) {
|
||||||
display: flex;
|
return keyword.slice(0, -1);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return keyword;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to standardize synonyms to a common keyword
|
// Function to standardize synonyms to a common keyword
|
||||||
function standardizeKeyword(keyword) {
|
function standardizeKeyword(keyword) {
|
||||||
const normalizedKeyword = normalizeKeyword(keyword);
|
const normalizedKeyword = normalizeKeyword(keyword);
|
||||||
for (let standard in productTypes) {
|
for (let standard in productTypes) {
|
||||||
if (productTypes[standard].includes(normalizedKeyword)) {
|
if (productTypes[standard].includes(normalizedKeyword)) {
|
||||||
return standard;
|
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 calculateRelevanceScore(product, keywords, query) {
|
||||||
function extractKeywords(description) {
|
let score = 0;
|
||||||
const keywordPrefix = "keywords: ";
|
let matchesAnyKeyword = false;
|
||||||
const keywordStart = description.toLowerCase().indexOf(keywordPrefix);
|
|
||||||
if (keywordStart !== -1) {
|
// Standardize and normalize keywords
|
||||||
return description.slice(keywordStart + keywordPrefix.length).trim().toLowerCase();
|
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
|
const searchInput = document.querySelector('#search-input');
|
||||||
fetch('https://merch.ookamikun.tv/.well-known/merchant-center/rss.xml')
|
const resultsContainer = document.querySelector('#search-results');
|
||||||
.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++) {
|
searchInput.addEventListener('input', function() {
|
||||||
const item = items[i];
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
const idElement = item.getElementsByTagName("g:id")[0];
|
const query = this.value.toLowerCase().trim();
|
||||||
const titleElement = item.getElementsByTagName("g:title")[0];
|
const keywords = query.split(/\s+/);
|
||||||
const descriptionElement = item.getElementsByTagName("g:description")[0];
|
const results = [];
|
||||||
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) {
|
for (let key in products) {
|
||||||
const id = idElement.textContent || '';
|
const product = products[key];
|
||||||
const title = titleElement.textContent.trim() || 'Untitled Product';
|
const relevanceScore = calculateRelevanceScore(product, keywords, query);
|
||||||
const description = descriptionElement.textContent.trim() || 'No description available.';
|
if (relevanceScore > 0) {
|
||||||
const link = linkElement.textContent || '#';
|
results.push({ product, relevanceScore });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
||||||
const colorMatches = standardizedKeywords.filter(keyword => product.color.includes(keyword));
|
|
||||||
if (colorMatches.length > 0) {
|
|
||||||
score += colorMatches.length * 10;
|
|
||||||
matchesAnyKeyword = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title match - lower weight
|
// Render the top X results
|
||||||
const titleMatches = standardizedKeywords.filter(keyword => product.title.toLowerCase().includes(keyword));
|
resultsContainer.innerHTML = '';
|
||||||
if (titleMatches.length > 0) {
|
results.slice(0, MAX_RESULTS).forEach(({ product }) => {
|
||||||
score += titleMatches.length * 5;
|
const minPrice = Math.min(...product.prices).toFixed(2);
|
||||||
matchesAnyKeyword = true;
|
const maxPrice = Math.max(...product.prices).toFixed(2);
|
||||||
}
|
const priceRange = minPrice === maxPrice ? `$${minPrice}` : `$${minPrice} - $${maxPrice}`;
|
||||||
|
|
||||||
// Description match - lower weight
|
const productElement = document.createElement('div');
|
||||||
const descriptionMatches = standardizedKeywords.filter(keyword => product.description.toLowerCase().includes(keyword));
|
productElement.className = 'search-result-item';
|
||||||
if (descriptionMatches.length > 0) {
|
productElement.innerHTML = `
|
||||||
score += descriptionMatches.length * 3;
|
<img src="${product.image}" alt="${product.title}" class="search-result-image">
|
||||||
matchesAnyKeyword = true;
|
<div class="search-result-info">
|
||||||
}
|
<h3>${product.title}</h3>
|
||||||
|
<p>${product.description.substring(0, 100)}...</p>
|
||||||
// Ensure the product is only considered if it contains at least one of the keywords
|
<div class="search-result-meta">
|
||||||
if (!matchesAnyKeyword) {
|
<span>Sizes: ${[...product.sizes].join(', ')}</span>
|
||||||
score = 0;
|
<span>Price: ${priceRange}</span>
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
resultsContainer.appendChild(productElement);
|
`;
|
||||||
});
|
resultsContainer.appendChild(productElement);
|
||||||
}, 500); // 0.5-second delay before performing the search
|
});
|
||||||
});
|
}, 500); // 0.5-second delay before performing the search
|
||||||
})
|
});
|
||||||
.catch(error => console.error('Error fetching or parsing the RSS feed:', error));
|
})
|
||||||
|
.catch(error => console.error('Error fetching or parsing the RSS feed:', error));
|
||||||
|
|
||||||
const searchIcon = document.querySelector('.header__icon--search');
|
const searchIcon = document.querySelector('.header__icon--search');
|
||||||
const searchBar = document.querySelector('#search-bar');
|
const searchBar = document.querySelector('#search-bar');
|
||||||
searchIcon.addEventListener('click', function() {
|
searchIcon.addEventListener('click', function() {
|
||||||
searchBar.classList.toggle('active');
|
searchBar.classList.toggle('active');
|
||||||
searchBar.querySelector('input').focus();
|
searchBar.querySelector('input').focus();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
});
|
||||||
|
|
Loading…
Reference in New Issue