Create search bar with 'Alternative to...' features

This commit is contained in:
Casey 2026-04-17 06:30:50 -05:00
parent 9c7f3b316d
commit ad9d816e16
3 changed files with 593 additions and 6 deletions

View File

@ -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;
}

View File

@ -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
View 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 &rarr;</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 &rarr;</a>' +
"</div>";
container.innerHTML = html;
}
// Initialize when DOM is ready
$(function () {
initSearch();
});