Sophia’s Affair Kansas City

About

Details

Reviews

Is this your business? Claim this listing to add photos, update info, and respond to reviews.

<script> /** * NaughtyPages — Open Now Badge * Reads business hours from GeoDirectory JSON-LD schema on listing pages * and injects "Open Now" / "Closed" badges on listing cards. * * Works on: * - Listing detail pages (gd_place single) * - Listing archive/search result cards * * No PHP required — pure client-side, reads openingHours from JSON-LD. * Deployed via WPCode Header & Footer Scripts plugin. */ (function() { 'use strict'; // ---- Helpers ------------------------------------------------------- /** * Parse "HH:MM" into minutes from midnight */ function parseTime(timeStr) { var parts = timeStr.split(':'); return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); } /** * Day abbreviations in schema.org format vs JS getDay() * Mo=1, Tu=2, We=3, Th=4, Fr=5, Sa=6, Su=0 */ var DAY_MAP = { Mo:1, Tu:2, We:3, Th:4, Fr:5, Sa:6, Su:0 }; var DAY_ABBR = ['Su','Mo','Tu','We','Th','Fr','Sa']; /** * Check if a venue is open right now given openingHours array * Each entry is like "Mo-Fr 09:00-17:00" or "Mo,We,Fr 10:00-22:00" or "Mo-Su 20:00-03:00" */ function isOpenNow(openingHours) { if (!openingHours || !openingHours.length) return null; // unknown var now = new Date(); var todayIdx = now.getDay(); // 0=Sun, 1=Mon, etc. var todayAbbr = DAY_ABBR[todayIdx]; var nowMinutes = now.getHours() * 60 + now.getMinutes(); for (var i = 0; i < openingHours.length; i++) { var entry = openingHours[i].trim(); // Parse "DAYS HH:MM-HH:MM" var match = entry.match(/^([A-Za-z,\-]+)\s+(\d{1,2}:\d{2})-(\d{1,2}:\d{2})$/); if (!match) continue; var daysStr = match[1]; var openMin = parseTime(match[2]); var closeMin = parseTime(match[3]); // Check if today is in the days string var todayIncluded = false; // Handle ranges like "Mo-Fr" or "Mo-Su" if (daysStr.match(/^[A-Za-z]{2}-[A-Za-z]{2}$/)) { var fromDay = DAY_MAP[daysStr.substring(0, 2)]; var toDay = DAY_MAP[daysStr.substring(3, 5)]; // Range wraps around (e.g. Su-Sa) — treat as all days if (fromDay <= toDay) { todayIncluded = (todayIdx >= fromDay && todayIdx <= toDay); } else { // wrap (e.g. Sa-Tu = Sa,Su,Mo,Tu) todayIncluded = (todayIdx >= fromDay || todayIdx <= toDay); } } else { // Comma-separated: "Mo,We,Fr" or single "Mo" var days = daysStr.split(','); for (var d = 0; d < days.length; d++) { if (DAY_MAP[days[d].trim()] === todayIdx) { todayIncluded = true; break; } } } if (!todayIncluded) continue; // Handle overnight hours (e.g. 20:00-03:00) if (closeMin < openMin) { // Overnight: open if now >= openMin OR now < closeMin if (nowMinutes >= openMin || nowMinutes < closeMin) return true; } else { if (nowMinutes >= openMin && nowMinutes < closeMin) return true; } } return false; } /** * Build and return a badge element */ function makeBadge(isOpen) { var badge = document.createElement('span'); if (isOpen) { badge.className = 'np-open-badge np-open'; badge.innerHTML = '<span class="np-open-dot"></span>Open Now'; } else { badge.className = 'np-open-badge np-closed'; badge.innerHTML = 'Closed'; } return badge; } // ---- CSS ----------------------------------------------------------- var css = ` .np-open-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em; padding: 3px 9px; border-radius: 20px; line-height: 1.4; white-space: nowrap; vertical-align: middle; margin-left: 6px; } .np-open-badge.np-open { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .np-open-badge.np-closed { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .np-open-dot { display: inline-block; width: 7px; height: 7px; background: #28a745; border-radius: 50%; animation: np-pulse 1.8s infinite; } @keyframes np-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } /* Listing detail page badge */ .np-open-badge-detail { display: inline-flex; align-items: center; gap: 6px; font-size: 0.82rem; font-weight: 700; padding: 5px 14px; border-radius: 20px; margin: 8px 0 12px; } .np-open-badge-detail.np-open { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .np-open-badge-detail.np-closed { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } `; var styleEl = document.createElement('style'); styleEl.textContent = css; document.head.appendChild(styleEl); // ---- Main Logic ---------------------------------------------------- function init() { // Find all JSON-LD LocalBusiness blocks on the page var scripts = document.querySelectorAll('script[type="application/ld+json"]'); // On a LISTING DETAIL page: inject badge near the business name/title scripts.forEach(function(script) { try { var data = JSON.parse(script.textContent); // Handle @graph arrays (Yoast) and direct objects (GeoDirectory) var items = data['@graph'] ? data['@graph'] : [data]; items.forEach(function(item) { if (item['@type'] === 'LocalBusiness' && item.openingHours) { var openStatus = isOpenNow( Array.isArray(item.openingHours) ? item.openingHours : [item.openingHours] ); if (openStatus === null) return; // On detail page: find the listing title area var titleEl = document.querySelector('.geodir-post-title h1, .entry-title, h1.page-title'); if (titleEl && !titleEl.querySelector('.np-open-badge')) { var badge = makeBadge(openStatus); badge.className = badge.className + '-detail ' + badge.className.split(' ').slice(1).join(' '); // Insert after title titleEl.parentNode.insertBefore(badge, titleEl.nextSibling); } } }); } catch(e) {} }); // On LISTING CARD pages (archive/search): inject into each listing card // GeoDirectory renders cards with class .geodir-post or similar // Cards don't have individual JSON-LD, but we can check via data- attrs or DOM structure // The openingHours appear in the card HTML as text like "Mo-Su 20:00-03:00" // Look for GeoDirectory listing cards var cards = document.querySelectorAll('.geodir-post, .gd-loop-listing, .geodir-entry'); if (cards.length) { cards.forEach(function(card) { // Skip if badge already added if (card.querySelector('.np-open-badge')) return; // Try to find hours text in the card // GeoDirectory renders hours in a .gd-bh-wrap or similar element var hoursWrap = card.querySelector('.gd-bh-wrap, .geodir-field-business_hours, [class*="business-hours"]'); if (!hoursWrap) return; // Try to get the openingHours from data attribute or hidden text var hoursText = hoursWrap.textContent || ''; // Parse out the time patterns var hourMatches = hoursText.match(/[A-Za-z]{2}(?:-[A-Za-z]{2})?\s+\d{1,2}:\d{2}-\d{1,2}:\d{2}/g); if (!hourMatches || !hourMatches.length) return; var openStatus = isOpenNow(hourMatches); if (openStatus === null) return; // Find a good place to inject the badge var titleEl = card.querySelector('.geodir-post-title a, .entry-title a, h2 a, h3 a'); if (titleEl && !titleEl.closest('.geodir-post-title').querySelector('.np-open-badge')) { var badge = makeBadge(openStatus); titleEl.parentNode.appendChild(badge); } }); } } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script> <script> /** * NaughtyPages — Trust Score Badge * Scores each listing based on profile completeness and shows a badge. * * Scoring criteria (max 100 points): * - Description present: +20 * - Photos present (1+): +20 * - Phone number present: +15 * - Website present: +10 * - Business hours present: +15 * - Reviews present: +20 * * Badge tiers: * 80-100: ★★★ Verified (green) * 60-79: ★★ Established (blue) * 40-59: ★ Basic (grey) * 0-39: (no badge shown) * * Runs on listing detail pages only. * Deployed via WPCode Header & Footer Scripts footer section. */ (function() { 'use strict'; // CSS for trust badges var css = ` .np-trust-badge { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700; padding: 4px 12px; border-radius: 20px; margin: 4px 0 10px; letter-spacing: 0.02em; cursor: help; } .np-trust-verified { background: #fff3cd; color: #856404; border: 1px solid #ffc107; } .np-trust-established { background: #cce5ff; color: #004085; border: 1px solid #b8daff; } .np-trust-basic { background: #f8f9fa; color: #6c757d; border: 1px solid #dee2e6; } .np-trust-badge .np-trust-icon { font-style: normal; } /* Trust score on listing cards */ .np-trust-card { display: inline-flex; align-items: center; font-size: 0.65rem; font-weight: 700; padding: 2px 7px; border-radius: 10px; margin-left: 4px; vertical-align: middle; } `; var styleEl = document.createElement('style'); styleEl.id = 'np-trust-score-css'; if (!document.getElementById('np-trust-score-css')) { styleEl.textContent = css; document.head.appendChild(styleEl); } function calculateTrustScore(context) { var score = 0; var details = []; // Description (20 pts) var descEl = context.querySelector('.geodir-post-excerpt, .geodir-post-content, .geodir-custom-fields-value'); if (descEl && descEl.textContent.trim().length > 50) { score += 20; details.push('Description'); } // Photos (20 pts) var photos = context.querySelectorAll('.geodir-images img, .geodir-post-thumbnail img, .gd-gallery img, .carousel-item img, [class*="gallery"] img'); if (photos.length > 0) { score += 20; details.push(photos.length + ' photo(s)'); } // Phone (15 pts) var phoneEl = context.querySelector('a[href^="tel:"], .geodir-field-phone, [class*="geodir"][class*="phone"]'); if (phoneEl && phoneEl.textContent.trim()) { score += 15; details.push('Phone'); } // Website (10 pts) var websiteEl = context.querySelector('.geodir-field-website a, .geodir-field-website_url a, [class*="geodir-field-website"]'); if (websiteEl) { score += 10; details.push('Website'); } // Business hours (15 pts) var hoursEl = context.querySelector('.geodir-business-hours, .gd-bh-wrap, [class*="business-hours"], [class*="gd-bh"]'); if (hoursEl && hoursEl.textContent.trim().length > 5) { score += 15; details.push('Hours'); } // Reviews (20 pts) - Check for rating elements var reviewEl = context.querySelector('.geodir-review, .gd-star-rating, [class*="rating"], [class*="review-count"]'); var ratingText = context.querySelector('[class*="rating"] .count, [class*="star-rating"]'); if (reviewEl || ratingText) { // Verify it's not just an empty rating widget var starFill = context.querySelector('.geodir-star-rating [style*="width:"]'); if (starFill) { var widthMatch = starFill.getAttribute('style').match(/width:\s*(\d+)/); if (widthMatch && parseInt(widthMatch[1]) > 0) { score += 20; details.push('Reviews'); } } } return { score: score, details: details }; } function getTierInfo(score) { if (score >= 80) return { label: 'Verified Listing', icon: '★★★', className: 'np-trust-verified', tooltip: 'This listing has complete information including photos, contact details, and hours.' }; if (score >= 60) return { label: 'Established', icon: '★★', className: 'np-trust-established', tooltip: 'This listing has most key information filled in.' }; if (score >= 40) return { label: 'Basic Listing', icon: '★', className: 'np-trust-basic', tooltip: 'This listing has basic information. Contact details may be incomplete.' }; return null; // Don't show badge for incomplete listings } function init() { // Only run on listing detail pages (gd_place single) var isListingPage = document.body.classList.contains('single-gd_place') || document.querySelector('.geodir-single') || document.querySelector('.geodir-post-meta-container'); if (!isListingPage) return; var context = document.querySelector('.geodir-post, .entry-content, main, .site-content') || document.body; var result = calculateTrustScore(context); var tier = getTierInfo(result.score); if (!tier) return; // Score too low, don't show badge var badge = document.createElement('div'); badge.className = 'np-trust-badge ' + tier.className; badge.title = tier.tooltip + ' (Score: ' + result.score + '/100)'; badge.innerHTML = '<em class="np-trust-icon">' + tier.icon + '</em> ' + tier.label; // Insert after the Open Now badge or after the title var titleEl = document.querySelector('.geodir-post-title h1, .entry-title, h1.page-title'); if (titleEl) { // Insert after the title or the open-now badge var openNowBadge = document.querySelector('.np-open-badge-detail'); var insertAfter = openNowBadge || titleEl; insertAfter.parentNode.insertBefore(badge, insertAfter.nextSibling); } else { // Fallback: prepend to main content var main = document.querySelector('main, .site-content, .entry-content'); if (main) main.insertBefore(badge, main.firstChild); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 100); // slight delay to let Open Now badge render first } })(); </script>