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