Stores Near You
Shows nearby retail stores in a horizontal carousel, sorted by distance when location is allowed, with store image, address, hours, and tap-to-open directions.
<div class="heading">
<div class="title-container">
<p class="title">Stores Near You</p>
<p class="sub-title">If you prefer shopping in person</p>
</div>
</div>
<div class="carousel"></div>/* ── Heading ──────────────────────────────────────────────── */
.heading {
padding: 1rem;
padding-top: 1.5rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: 600;
padding-bottom: 0.2rem;
}
.sub-title {
font-size: 14px;
font-weight: 500;
color: gray;
}
.view-all-container {
display: flex;
align-items: center;
column-gap: 0.3rem;
cursor: pointer;
}
.view-all {
font-size: 14px;
font-weight: 500;
color: gray;
}
.arrow {
width: 1.3rem;
min-height: 1.3rem;
max-height: 1.3rem;
}
/* ── Carousel ─────────────────────────────────────────────── */
.carousel {
display: flex;
overflow-x: auto;
padding-left: 1rem;
padding-right: 1rem;
column-gap: 1rem;
padding-bottom: 1.5rem;
}
.carousel::-webkit-scrollbar {
display: none;
}
/* ── Card ─────────────────────────────────────────────────── */
.card {
/* flex: 0 0 160px; */
display: flex;
flex-direction: column;
background-color: #ffffff;
border-radius: 1rem;
overflow: hidden;
scroll-snap-align: start;
cursor: pointer;
min-width: 15rem;
max-width: 15rem;
height: fit-content;
}
/* ── Image ────────────────────────────────────────────────── */
.nf-img-wrap {
position: relative;
width: 100%;
aspect-ratio: 1 / 0.8;
overflow: hidden;
flex-shrink: 0;
background-color: #ededea;
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.nf-badge {
position: absolute;
top: 8px;
left: 8px;
background-color: #1a1a18;
color: #f5f3ec;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 3px 7px;
border-radius: 4px;
}
/* ── Card body ────────────────────────────────────────────── */
.card-body {
padding: 0.8rem;
display: flex;
flex-direction: column;
gap: 5px;
}
.name {
font-size: 0.9rem;
font-weight: 600;
color: #1a1a18;
line-height: 1.25;
}
.address {
font-size: 0.8rem;
font-weight: 400;
color: #6b6b66;
line-height: 1.5;
}
.divider {
height: 1px;
background-color: #f5f4f0;
margin-top: 0.3rem;
margin-bottom: 0.5rem;
}
.hours-label {
font-size: 0.6rem;
font-weight: 600;
color: #9e9d98;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.hours {
font-size: 0.8rem;
font-weight: 400;
color: #6b6b66;
line-height: 1.55;
}
/* ── Skeleton ─────────────────────────────────────────────── */
@keyframes sf-skeleton-shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.sf-skeleton {
background: linear-gradient(90deg, #ececec 25%, #f5f5f5 50%, #ececec 75%);
background-size: 200% 100%;
animation: sf-skeleton-shimmer 1.4s ease-in-out infinite;
border-radius: 6px;
}
.sf-skeleton-img {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 0;
}
.sf-skeleton-name {
width: 70%;
height: 0.8rem;
margin-top: 0.3rem;
}
.sf-skeleton-addr {
width: 90%;
height: 0.7rem;
margin-top: 0.2rem;
}
.sf-skeleton-addr2 {
width: 60%;
height: 0.7rem;
margin-top: 0.15rem;
}
.sf-skeleton-hours {
width: 85%;
height: 0.65rem;
margin-top: 0.35rem;
}
.sf-skeleton-body {
padding: 10px 12px 14px;
display: flex;
flex-direction: column;
gap: 0;
}const stores = [
{
name: "Murfreesboro",
address: "1250 NW Broad St\nMurfreesboro, TN 37129",
hours: "Mon to Sat: 9AM - 8PM\nSun: 10AM - 6PM",
badge: "Superstore",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Boro-Superstore-Storefront_01_9df1c776-67ac-4843-9d7d-dc3f36fd2777.jpg?v=1767984826&width=400",
url: "https://maps.app.goo.gl/WphZwP1TPanhzEi66",
lat: 35.8446,
lng: -86.4072
},
{
name: "Cookeville",
address: "650 S Jefferson Ave. #105\nCookeville, TN 38501",
hours: "Mon to Sat: 9AM - 8PM\nSun: 10AM - 7PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Cookeville-Storefront_01_a246ae4c-5385-44e1-86db-50c2653adb31.jpg?v=1767984865&width=400",
url: "https://maps.app.goo.gl/hL6YQvNrzEcUPPEk7",
lat: 36.1503,
lng: -85.5024
},
{
name: "Jackson",
address: "1296 Union University Dr.\nJackson, TN 38305",
hours: "Mon to Sat: 9AM - 8PM\nSun: 11AM - 6PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Jackson-Storefront_01_6d3f2ce4-1db1-4d70-ae78-e7b03d4121f7.jpg?v=1767984905&width=400",
url: "https://maps.app.goo.gl/KiuRyztPmkEoaMWz7",
lat: 35.6143,
lng: -88.8350
},
{
name: "Ooltewah",
address: "6053 Artesian Circle\nOoltewah, TN 37363",
hours: "Mon to Fri: 9AM - 8PM\nSat: 9AM - 7PM\nSun: 12PM - 6PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Ooltewah-Storefront_01_37b2fc12-e99d-4c38-b1ab-e61980f418f3.jpg?v=1767984947&width=400",
url: "https://maps.app.goo.gl/t5RCdp8zp9QQDhZH8",
lat: 35.0793,
lng: -85.0691
},
{
name: "Clarksville",
address: "2766 Wilma Rudolph Blvd.\nClarksville, TN 37040",
hours: "Mon to Sat: 9AM - 8PM\nSun: 12PM - 7PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Clarksville-Storefront_01_675e6e16-8984-4c04-81f2-b9621aa03b2b.jpg?v=1767985046&width=400",
url: "https://maps.app.goo.gl/WwUaW1CEEjCGH3mg6",
lat: 36.5290,
lng: -87.3595
},
{
name: "Cool Springs",
address: "790 Jordan Rd. #104\nFranklin, TN 37067",
hours: "Mon to Fri: 9AM - 8PM\nSat: 9AM - 6PM\nSun: 12PM - 6PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Franklin-Storefront_01_e447b4e5-8506-4139-845c-b94b297d3b26.jpg?v=1767985084&width=400",
url: "https://maps.app.goo.gl/WvSQLUcoRBBmW2oG8",
lat: 35.9251,
lng: -86.8500
},
{
name: "Hendersonville",
address: "202 Anderson Ln #101\nHendersonville, TN 37075",
hours: "Mon to Sat: 9AM - 8PM\nSun: 12PM - 7PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Hendersonville-Storefront_01_c37ea9f3-ee14-4cbd-9f28-ee9185150187.jpg?v=1767985107&width=400",
url: "https://maps.app.goo.gl/WvSQLUcoRBBmW2oG8",
lat: 36.3001,
lng: -86.6200
},
{
name: "Nashville",
address: "7048 Hwy 70 S\nBellevue, TN 37221",
hours: "Mon to Sat: 9AM - 8PM\nSun: 12PM - 7PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Bellevue-Storefront_01_40f968d0-fb7a-4736-be4f-e3d7aa5a5462.jpg?v=1767985136&width=400",
url: "https://maps.app.goo.gl/HiNmypH7N7kPVS7o7",
lat: 36.0697,
lng: -86.9290
},
{
name: "Smyrna",
address: "303 Sam Ridley Pkwy W\nSmyrna, TN 37167",
hours: "Mon to Fri: 9AM - 8PM\nSat: 9AM - 6PM\nSun: 12PM - 6PM",
image: "https://cdn.shopify.com/s/files/1/2640/1510/files/NF-Smyrna-Storefront_01_3409ba6f-babe-49a9-a331-8d89d60010b4.jpg?v=1767985184&width=400",
url: "https://maps.app.goo.gl/PC8gWoQdtjnjGNbP7",
lat: 35.9831,
lng: -86.5190
}
];
// ── Haversine distance (miles) ─────────────────────────────
function haversineDistance(lat1, lng1, lat2, lng2) {
const R = 3958.8; // Earth radius in miles
const toRad = deg => deg * Math.PI / 180;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// ── Render helpers ─────────────────────────────────────────
function renderSkeletons(count) {
return Array(count).fill(null).map(() => `
<div class="card">
<div class="sf-skeleton sf-skeleton-img"></div>
<div class="sf-skeleton-body">
<div class="sf-skeleton sf-skeleton-name"></div>
<div class="sf-skeleton sf-skeleton-addr"></div>
<div class="sf-skeleton sf-skeleton-addr2"></div>
<div class="sf-skeleton sf-skeleton-hours" style="margin-top:0.5rem;"></div>
<div class="sf-skeleton sf-skeleton-hours" style="width:65%;margin-top:0.15rem;"></div>
</div>
</div>
`).join('');
}
function renderCards(storeList) {
return storeList.map(s => {
const badge = s.badge ? `<span class="nf-badge">${s.badge}</span>` : '';
const address = s.address.replace(/\n/g, '<br>');
const hours = s.hours.replace(/\n/g, '<br>');
const distanceLabel = s.distanceMiles != null
? `<p class="hours-label" style="margin-top:0.3rem;">${s.distanceMiles.toFixed(1)} mi away</p>`
: '';
return `
<div class="card" onclick="openStore('${s.url}')">
<div class="nf-img-wrap">
<img class="image" src="${s.image}" alt="${s.name}" />
${badge}
</div>
<div class="card-body">
<p class="name">${s.name}</p>
<p class="address">${address}</p>
<div class="divider"></div>
<p class="hours-label">Hours</p>
<p class="hours">${hours}</p>
${distanceLabel}
</div>
</div>`;
}).join('');
}
function preloadAndRender(storeList) {
const carousel = document.querySelector('.carousel');
carousel.innerHTML = renderSkeletons(storeList.length);
const imagePromises = storeList.map(store => new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(store);
img.onerror = () => resolve(store); // still render on error
img.src = store.image;
}));
Promise.all(imagePromises).then(loaded => {
carousel.innerHTML = renderCards(loaded);
});
}
// ── Initial render (default order) ────────────────────────
preloadAndRender(stores);
// ── Locate & sort ──────────────────────────────────────────
function normalizeLocationResponse(response) {
const data = response?.data || response?.location || response || {};
return {
latitude: data.latitude ?? data.lat ?? data.coords?.latitude ?? null,
longitude: data.longitude ?? data.lng ?? data.coords?.longitude ?? null,
};
}
async function locateStore() {
try {
// 1. Check permission (it's a string: "granted" | "denied" | "unknown")
let permission = VajroSDK.variables.device?.permissions?.location;
// 2. Request if not yet granted
if (permission !== "granted") {
const response = await VajroSDK.actions.requestLocationPermission();
if (response?.status === "failed") {
return;
}
}
// 3. Fetch location
const response = await VajroSDK.actions.getDeviceLocation();
const { latitude, longitude } = normalizeLocationResponse(response);
if (latitude == null || longitude == null) {
return;
}
// 5. Sort and render
const sorted = stores
.map(s => ({
...s,
distanceMiles: haversineDistance(latitude, longitude, s.lat, s.lng)
}))
.sort((a, b) => a.distanceMiles - b.distanceMiles);
preloadAndRender(sorted);
} catch (err) {
console.warn('locateStore error:', err);
}
}
document.addEventListener("DOMContentLoaded", () => {
locateStore();
});
// ── Misc ───────────────────────────────────────────────────
function openStore(url) {
Superfans.actions.openUrl({ path: url });
}