Update search-script.js

main
kami 2024-08-10 02:13:28 +00:00
parent 0890e90aef
commit 2e898ce357
1 changed files with 193 additions and 245 deletions

View File

@ -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];
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];
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const query = this.value.toLowerCase().trim();
const keywords = query.split(/\s+/);
const results = [];
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>
});