Create search bar with 'Alternative to...' features
This commit is contained in:
parent
9c7f3b316d
commit
ad9d816e16
166
css/styles.css
166
css/styles.css
@ -2,6 +2,158 @@
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
/* Search Hero */
|
||||
.search-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-top: 1.5rem;
|
||||
z-index: 50;
|
||||
}
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fefff1;
|
||||
border-radius: 100px;
|
||||
padding: 4px 20px;
|
||||
box-shadow: rgba(48, 46, 53, 0.4) 0 2px 4px,
|
||||
rgba(45, 35, 66, 0.3) 0 7px 13px -3px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.search-bar:focus-within {
|
||||
box-shadow: #45e3ff 0 4px 12px, rgba(45, 35, 66, 0.3) 0 7px 13px 3px;
|
||||
}
|
||||
.search-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #3b444b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 14px 12px;
|
||||
font-size: 16px;
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
color: #1e2733;
|
||||
outline: none;
|
||||
}
|
||||
.search-bar input::placeholder {
|
||||
color: #7a8490;
|
||||
font-size: 14px;
|
||||
}
|
||||
.search-results {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #212b38;
|
||||
border-radius: 16px;
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0 8px 24px;
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.search-results.active {
|
||||
display: block;
|
||||
}
|
||||
.search-result-item {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.search-result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.search-result-header h4 {
|
||||
margin: 0;
|
||||
color: #fefff1;
|
||||
font-size: 1.2em;
|
||||
text-align: left;
|
||||
}
|
||||
.search-result-price {
|
||||
color: #a162f7;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.search-result-item p {
|
||||
color: #c0c8d4;
|
||||
margin: 8px 0 12px;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.search-alt-badge {
|
||||
display: inline-block;
|
||||
background: rgba(161, 98, 247, 0.2);
|
||||
color: #c89aff;
|
||||
border-radius: 100px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.search-result-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.search-result-actions .button-3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
height: 38px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
.search-learn-more {
|
||||
color: #45e3ff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
.search-learn-more:hover {
|
||||
color: #7eeeff;
|
||||
}
|
||||
.search-no-results {
|
||||
text-align: center;
|
||||
}
|
||||
.search-no-results h4 {
|
||||
color: #fefff1;
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.search-no-results p {
|
||||
text-align: center;
|
||||
color: #c0c8d4;
|
||||
}
|
||||
.search-no-results .button-3 {
|
||||
margin: 12px auto 0;
|
||||
}
|
||||
.search-cta {
|
||||
text-align: center;
|
||||
background: rgba(161, 98, 247, 0.06);
|
||||
}
|
||||
.search-cta p {
|
||||
text-align: center;
|
||||
color: #a0a8b4;
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.search-cta .search-learn-more {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1e2733;
|
||||
margin: 0;
|
||||
@ -581,6 +733,8 @@ input:checked + .drop + .menu-items {
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
.align-right {
|
||||
display: flex;
|
||||
@ -598,6 +752,18 @@ nav a:not(.logo) {
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.search-hero {
|
||||
max-width: 100%;
|
||||
}
|
||||
.search-bar input::placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
.search-results {
|
||||
border-radius: 12px;
|
||||
}
|
||||
.search-result-item {
|
||||
padding: 16px;
|
||||
}
|
||||
.flex-column {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
19
index.html
19
index.html
@ -50,6 +50,7 @@
|
||||
></script>
|
||||
<script src="./js/liveChat.js"></script>
|
||||
<script src="./js/main.js"></script>
|
||||
<script src="./js/search.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -64,12 +65,18 @@
|
||||
<h2 class="top">
|
||||
Cloud hosting for the last hour so you can follow your dreams.
|
||||
</h2>
|
||||
<a
|
||||
class="button-3 top"
|
||||
role="button"
|
||||
href="https://my.lasthourhosting.org/register"
|
||||
>Get Started
|
||||
</a>
|
||||
<div class="search-hero top">
|
||||
<div class="search-bar">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
id="service-search-input"
|
||||
placeholder='What do you need help with? Try "alternative to Shopify"'
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column align-right">
|
||||
<video width="100%" controls>
|
||||
|
||||
414
js/search.js
Normal file
414
js/search.js
Normal file
@ -0,0 +1,414 @@
|
||||
// Service search data with keyword matching and "alternative to" support
|
||||
const searchServices = [
|
||||
{
|
||||
name: "Web Hosting",
|
||||
price: "$29/mo.",
|
||||
description:
|
||||
"Build and launch impressive WordPress websites with dedicated features, tools, and guidance.",
|
||||
link: "/web-hosting.html",
|
||||
orderLink:
|
||||
"https://my.lasthourhosting.org/products/web-hosting",
|
||||
keywords: [
|
||||
"website",
|
||||
"blog",
|
||||
"wordpress",
|
||||
"cms",
|
||||
"site",
|
||||
"landing page",
|
||||
"portfolio",
|
||||
"web hosting",
|
||||
"web",
|
||||
"hosting",
|
||||
"publish",
|
||||
"domain",
|
||||
"webpage",
|
||||
"church website",
|
||||
"business website",
|
||||
"personal site",
|
||||
],
|
||||
alternativeTo: [
|
||||
"squarespace",
|
||||
"wix",
|
||||
"godaddy",
|
||||
"bluehost",
|
||||
"siteground",
|
||||
"hostgator",
|
||||
"wordpress.com",
|
||||
"weebly",
|
||||
"dreamhost",
|
||||
"a2 hosting",
|
||||
"ionos",
|
||||
"namecheap",
|
||||
"hostinger",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "E-Commerce",
|
||||
price: "$29/mo.",
|
||||
description:
|
||||
"Get a stunning online store with PrestaShop — built to sell, whether from scratch or pre-built themes.",
|
||||
link: "/e-commerce.html",
|
||||
orderLink:
|
||||
"https://my.lasthourhosting.org/products/e-commerce",
|
||||
keywords: [
|
||||
"store",
|
||||
"shop",
|
||||
"sell",
|
||||
"online store",
|
||||
"products",
|
||||
"ecommerce",
|
||||
"e-commerce",
|
||||
"shopping cart",
|
||||
"payments",
|
||||
"retail",
|
||||
"inventory",
|
||||
"checkout",
|
||||
"merchandise",
|
||||
"order",
|
||||
"catalog",
|
||||
"prestashop",
|
||||
],
|
||||
alternativeTo: [
|
||||
"shopify",
|
||||
"bigcommerce",
|
||||
"magento",
|
||||
"woocommerce",
|
||||
"etsy",
|
||||
"squarespace commerce",
|
||||
"wix stores",
|
||||
"ecwid",
|
||||
"volusion",
|
||||
"3dcart",
|
||||
"gumroad",
|
||||
"square online",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Workspace Tools",
|
||||
price: "$49/mo.",
|
||||
description:
|
||||
"A secure, private Nextcloud workspace for up to 100 users — files, calendars, contacts, and full data ownership.",
|
||||
link: "/workspace-tools.html",
|
||||
orderLink:
|
||||
"https://my.lasthourhosting.org/products/workspace",
|
||||
keywords: [
|
||||
"files",
|
||||
"storage",
|
||||
"cloud storage",
|
||||
"collaboration",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"documents",
|
||||
"drive",
|
||||
"office",
|
||||
"workspace",
|
||||
"nextcloud",
|
||||
"share",
|
||||
"sync",
|
||||
"team",
|
||||
"productivity",
|
||||
"document",
|
||||
],
|
||||
alternativeTo: [
|
||||
"google workspace",
|
||||
"google drive",
|
||||
"microsoft 365",
|
||||
"office 365",
|
||||
"dropbox",
|
||||
"onedrive",
|
||||
"box",
|
||||
"icloud",
|
||||
"notion",
|
||||
"sharepoint",
|
||||
"zoho",
|
||||
"confluence",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Private Email",
|
||||
price: "$29/mo.",
|
||||
description:
|
||||
"Secure, dedicated, ad-free email with Mailu — full control over your inbox and data.",
|
||||
link: "/private-email.html",
|
||||
orderLink:
|
||||
"https://my.lasthourhosting.org/products/private-email",
|
||||
keywords: [
|
||||
"email",
|
||||
"inbox",
|
||||
"mail",
|
||||
"messaging",
|
||||
"communication",
|
||||
"smtp",
|
||||
"imap",
|
||||
"mailu",
|
||||
"ad-free",
|
||||
"private email",
|
||||
"custom email",
|
||||
"business email",
|
||||
],
|
||||
alternativeTo: [
|
||||
"gmail",
|
||||
"outlook",
|
||||
"yahoo mail",
|
||||
"protonmail",
|
||||
"proton mail",
|
||||
"tutanota",
|
||||
"hey",
|
||||
"fastmail",
|
||||
"zoho mail",
|
||||
"aol",
|
||||
"icloud mail",
|
||||
"mailchimp",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "VPS Hosting",
|
||||
price: "Custom",
|
||||
description:
|
||||
"Powerful Linux VPS with SSH access, IPv4 + IPv6, full KVM hypervisor, and no lock-in.",
|
||||
link: "/vps-hosting.html",
|
||||
orderLink:
|
||||
"https://my.lasthourhosting.org/products/vps",
|
||||
keywords: [
|
||||
"server",
|
||||
"vps",
|
||||
"linux",
|
||||
"ssh",
|
||||
"virtual server",
|
||||
"dedicated",
|
||||
"deploy",
|
||||
"app",
|
||||
"application",
|
||||
"database",
|
||||
"docker",
|
||||
"container",
|
||||
"root access",
|
||||
"kvm",
|
||||
"virtual machine",
|
||||
"vm",
|
||||
"cloud server",
|
||||
"node",
|
||||
"backend",
|
||||
"api",
|
||||
],
|
||||
alternativeTo: [
|
||||
"aws",
|
||||
"amazon web services",
|
||||
"azure",
|
||||
"digitalocean",
|
||||
"digital ocean",
|
||||
"linode",
|
||||
"vultr",
|
||||
"hetzner",
|
||||
"ovh",
|
||||
"google cloud",
|
||||
"gcp",
|
||||
"lightsail",
|
||||
"render",
|
||||
"railway",
|
||||
"fly.io",
|
||||
"heroku",
|
||||
"vercel",
|
||||
"netlify",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Data Migration",
|
||||
price: "Contact for Quote",
|
||||
description:
|
||||
"We'll help you migrate your assets to our systems. Contact us for a custom quote.",
|
||||
link: "/contact.html",
|
||||
orderLink: "/contact.html",
|
||||
keywords: [
|
||||
"migrate",
|
||||
"transfer",
|
||||
"move",
|
||||
"switch",
|
||||
"import",
|
||||
"export",
|
||||
"migration",
|
||||
"move over",
|
||||
"switching",
|
||||
"transition",
|
||||
],
|
||||
alternativeTo: [],
|
||||
},
|
||||
];
|
||||
|
||||
function initSearch() {
|
||||
const input = document.getElementById("service-search-input");
|
||||
const results = document.getElementById("search-results");
|
||||
if (!input || !results) return;
|
||||
|
||||
input.addEventListener("input", function () {
|
||||
const query = this.value.trim().toLowerCase();
|
||||
if (query.length < 2) {
|
||||
results.classList.remove("active");
|
||||
results.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = findMatches(query);
|
||||
renderResults(matches, query, results);
|
||||
results.classList.add("active");
|
||||
});
|
||||
|
||||
// Close results on click outside
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!e.target.closest(".search-hero")) {
|
||||
results.classList.remove("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Re-open on focus if there's input
|
||||
input.addEventListener("focus", function () {
|
||||
if (this.value.trim().length >= 2) {
|
||||
const matches = findMatches(this.value.trim().toLowerCase());
|
||||
renderResults(matches, this.value.trim().toLowerCase(), results);
|
||||
results.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findMatches(query) {
|
||||
// Check for "alternative to" pattern
|
||||
const altMatch = query.match(
|
||||
/(?:alternative\s+to|replace|replacing|switch\s+from|instead\s+of|competitor\s+to|like)\s+(.+)/i
|
||||
);
|
||||
const altQuery = altMatch ? altMatch[1].trim() : null;
|
||||
|
||||
const scored = searchServices.map(function (service) {
|
||||
let score = 0;
|
||||
|
||||
if (altQuery) {
|
||||
// Match against alternativeTo list
|
||||
for (var i = 0; i < service.alternativeTo.length; i++) {
|
||||
if (service.alternativeTo[i].indexOf(altQuery) !== -1) {
|
||||
score += 100;
|
||||
break;
|
||||
}
|
||||
if (altQuery.indexOf(service.alternativeTo[i]) !== -1) {
|
||||
score += 80;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match against keywords
|
||||
for (var j = 0; j < service.keywords.length; j++) {
|
||||
if (service.keywords[j].indexOf(query) !== -1) {
|
||||
score += 50;
|
||||
}
|
||||
if (query.indexOf(service.keywords[j]) !== -1 && service.keywords[j].length > 2) {
|
||||
score += 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Match against service name
|
||||
if (service.name.toLowerCase().indexOf(query) !== -1) {
|
||||
score += 60;
|
||||
}
|
||||
|
||||
// Match against description
|
||||
if (service.description.toLowerCase().indexOf(query) !== -1) {
|
||||
score += 20;
|
||||
}
|
||||
|
||||
// Match against alternativeTo even without "alternative to" prefix
|
||||
for (var k = 0; k < service.alternativeTo.length; k++) {
|
||||
if (service.alternativeTo[k].indexOf(query) !== -1 && query.length > 2) {
|
||||
score += 40;
|
||||
}
|
||||
}
|
||||
|
||||
return { service: service, score: score };
|
||||
});
|
||||
|
||||
scored.sort(function (a, b) {
|
||||
return b.score - a.score;
|
||||
});
|
||||
|
||||
return scored.filter(function (item) {
|
||||
return item.score > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function renderResults(matches, query, container) {
|
||||
if (matches.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="search-result-item search-no-results">' +
|
||||
"<h4>Not sure what you need?</h4>" +
|
||||
'<p>Let\'s figure it out together — our team is here to help.</p>' +
|
||||
'<a class="button-3" href="/contact.html">Talk to Our Team</a>' +
|
||||
"</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is an "alternative to" query
|
||||
var altMatch = query.match(
|
||||
/(?:alternative\s+to|replace|replacing|switch\s+from|instead\s+of|competitor\s+to|like)\s+(.+)/i
|
||||
);
|
||||
var altProduct = altMatch ? altMatch[1].trim() : null;
|
||||
|
||||
var html = "";
|
||||
var shown = matches.slice(0, 3);
|
||||
|
||||
for (var i = 0; i < shown.length; i++) {
|
||||
var s = shown[i].service;
|
||||
var altLabel = "";
|
||||
if (altProduct) {
|
||||
// Find which specific product it's an alternative to
|
||||
for (var j = 0; j < s.alternativeTo.length; j++) {
|
||||
if (
|
||||
s.alternativeTo[j].indexOf(altProduct) !== -1 ||
|
||||
altProduct.indexOf(s.alternativeTo[j]) !== -1
|
||||
) {
|
||||
altLabel =
|
||||
'<span class="search-alt-badge">Alternative to ' +
|
||||
s.alternativeTo[j].charAt(0).toUpperCase() +
|
||||
s.alternativeTo[j].slice(1) +
|
||||
"</span>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html +=
|
||||
'<div class="search-result-item">' +
|
||||
'<div class="search-result-header">' +
|
||||
"<h4>" +
|
||||
s.name +
|
||||
"</h4>" +
|
||||
'<span class="search-result-price">' +
|
||||
s.price +
|
||||
"</span>" +
|
||||
"</div>" +
|
||||
altLabel +
|
||||
"<p>" +
|
||||
s.description +
|
||||
"</p>" +
|
||||
'<div class="search-result-actions">' +
|
||||
'<a class="button-3" href="' +
|
||||
s.orderLink +
|
||||
'">Get Started</a>' +
|
||||
'<a class="search-learn-more" href="' +
|
||||
s.link +
|
||||
'">Learn More →</a>' +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
// Always show a "Talk to us" option at the bottom
|
||||
html +=
|
||||
'<div class="search-result-item search-cta">' +
|
||||
"<p>Need something custom or want to discuss your options?</p>" +
|
||||
'<a class="search-learn-more" href="/contact.html">Talk to Our Team →</a>' +
|
||||
"</div>";
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
$(function () {
|
||||
initSearch();
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user