// ============================================
// COMBINED JAVASCRIPT - ALL JS FILES
// ============================================
// --- DATA URL (External JSON) ---
const DATA_URL = "https://cdn.glowradi.us/case-study-md/case-studies.json";
// ============================================
// SCRIPT.JS - Index Page Logic
// ============================================
(() => {
const state = {
caseStudies: [],
filteredStudies: [],
currentPage: 1,
pageSize: 6,
activeFilters: {
search: "",
geo: new Set(),
deal: new Set(),
channel: new Set(),
maturity: new Set(),
pain_point: new Set(),
solution_interest: new Set(),
ad_free_only: false
}
};
const elements = {
cardsGrid: document.getElementById("cards-grid"),
countBadge: document.getElementById("count-badge"),
pagination: document.getElementById("pagination"),
noResults: document.getElementById("no-results"),
loadError: document.getElementById("load-error"),
searchInput: document.getElementById("search-input"),
adFreeToggle: document.getElementById("ad-free-toggle"),
resetBtn: document.getElementById("reset-filters")
};
const GRADIENT_PALETTE = [
"linear-gradient(135deg, #2563eb 0%, #1e40af 100%)",
"linear-gradient(135deg, #059669 0%, #064e3b 100%)",
"linear-gradient(135deg, #7c3aed 0%, #4c1d95 100%)",
"linear-gradient(135deg, #ea580c 0%, #9a3412 100%)"
];
function getGradientIndex(seed = "") {
return Array.from(seed).reduce((hash, char) => hash + char.charCodeAt(0), 0) % GRADIENT_PALETTE.length;
}
function ensureArray(value) {
if (Array.isArray(value)) return value;
if (typeof value === "string" && value.trim()) return [value];
return [];
}
function joinValues(values) {
const items = ensureArray(values);
return items.length ? items.join(", ") : "--";
}
function normalizeFilterData(filterData = {}) {
return {
geo: ensureArray(filterData.geo || filterData.region),
deal: ensureArray(filterData.deal || filterData.deal_size),
channel: ensureArray(filterData.channel),
maturity: ensureArray(filterData.maturity || filterData.business_model),
pain_point: ensureArray(filterData.pain_point || filterData.pain_points),
solution_interest: ensureArray(filterData.solution_interest || filterData.solution_paths),
is_ad_free: filterData.is_ad_free === true
};
}
function debounce(fn, delay) {
let timer;
return (...args) => {
window.clearTimeout(timer);
timer = window.setTimeout(() => fn(...args), delay);
};
}
function setCount(count) {
elements.countBadge.textContent = `(${count})`;
}
function clearGrid() {
elements.cardsGrid.innerHTML = "";
}
function renderCards(data) {
clearGrid();
if (!data.length) {
elements.noResults.classList.remove("hidden");
setCount(0);
return;
}
elements.noResults.classList.add("hidden");
setCount(data.length);
const fragment = document.createDocumentFragment();
data.forEach((item) => {
const normalized = normalizeFilterData(item.filter_data);
const card = document.createElement("article");
card.className = "card";
const image = document.createElement("div");
image.className = "card-image";
const gradientIndex = getGradientIndex(item.id || item.meta?.title || "");
image.style.background = GRADIENT_PALETTE[gradientIndex];
const imageText = document.createElement("span");
imageText.className = "card-image-text";
imageText.textContent = item.meta.title;
image.appendChild(imageText);
const body = document.createElement("div");
body.className = "card-body";
const title = document.createElement("h3");
title.className = "card-title";
title.textContent = item.meta.title;
const summary = document.createElement("p");
summary.className = "card-summary";
summary.textContent = item.meta.summary;
const specGrid = document.createElement("div");
specGrid.className = "spec-grid";
const dealItem = document.createElement("div");
dealItem.className = "spec-item";
const dealLabel = document.createElement("span");
dealLabel.className = "spec-label";
dealLabel.textContent = "Deal";
const dealValue = document.createElement("span");
dealValue.className = "spec-value";
dealValue.textContent = joinValues(normalized.deal);
dealItem.append(dealLabel, dealValue);
const timelineItem = document.createElement("div");
timelineItem.className = "spec-item";
const timelineLabel = document.createElement("span");
timelineLabel.className = "spec-label";
timelineLabel.textContent = "Maturity";
const timelineValue = document.createElement("span");
timelineValue.className = "spec-value";
timelineValue.textContent = joinValues(normalized.maturity);
timelineItem.append(timelineLabel, timelineValue);
const strategyItem = document.createElement("div");
strategyItem.className = "spec-item";
strategyItem.style.gridColumn = "1 / -1";
strategyItem.style.marginTop = "4px";
const strategyLabel = document.createElement("span");
strategyLabel.className = "spec-label";
strategyLabel.textContent = "Solution Interest";
const strategyValue = document.createElement("span");
strategyValue.className = "spec-value";
strategyValue.textContent = joinValues(normalized.solution_interest.slice(0, 2));
strategyItem.append(strategyLabel, strategyValue);
specGrid.append(dealItem, timelineItem, strategyItem);
body.append(title, summary, specGrid);
const footer = document.createElement("div");
footer.className = "card-footer";
const client = document.createElement("div");
client.className = "client-logo";
const logo = document.createElement("img");
logo.src = item.meta.logo_url;
logo.alt = `${item.meta.client_name} logo`;
const clientName = document.createElement("span");
clientName.textContent = item.meta.client_name;
client.append(logo, clientName);
const read = document.createElement("a");
read.className = "read-btn";
const fallbackSlug = `case-study.html?id=${encodeURIComponent(item.id)}`;
read.href = item.meta.slug && item.meta.slug !== "#" ? item.meta.slug : fallbackSlug;
read.rel = "noopener noreferrer";
read.textContent = "View Study →";
footer.append(client, read);
card.append(image, body, footer);
fragment.appendChild(card);
});
elements.cardsGrid.appendChild(fragment);
}
function matchesGroup(itemValues, selectedValues) {
if (selectedValues.size === 0) return true;
return itemValues.some((value) => selectedValues.has(value));
}
function renderPagination(totalItems) {
if (!elements.pagination) return;
elements.pagination.innerHTML = "";
const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize));
if (totalPages <= 1) return; const prevBtn=document.createElement("button"); prevBtn.textContent="Prev" ; prevBtn.disabled=state.currentPage===1; prevBtn.addEventListener("click", ()=> {
state.currentPage = Math.max(1, state.currentPage - 1);
renderPage();
});
const nextBtn = document.createElement("button");
nextBtn.textContent = "Next";
nextBtn.disabled = state.currentPage === totalPages;
nextBtn.addEventListener("click", () => {
state.currentPage = Math.min(totalPages, state.currentPage + 1);
renderPage();
});
elements.pagination.appendChild(prevBtn);
for (let page = 1; page <= totalPages; page +=1) { const pageBtn=document.createElement("button"); pageBtn.textContent=String(page); if (page===state.currentPage) { pageBtn.classList.add("is-active"); pageBtn.disabled=true; } pageBtn.addEventListener("click", ()=> {
state.currentPage = page;
renderPage();
});
elements.pagination.appendChild(pageBtn);
}
elements.pagination.appendChild(nextBtn);
}
function renderPage() {
const totalItems = state.filteredStudies.length;
const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize));
state.currentPage = Math.min(state.currentPage, totalPages);
const start = (state.currentPage - 1) * state.pageSize;
const pageItems = state.filteredStudies.slice(start, start + state.pageSize);
renderCards(pageItems);
renderPagination(totalItems);
}
function filterData() {
const searchTerm = state.activeFilters.search.trim();
const filtered = state.caseStudies.filter((item) => {
const meta = item.meta || {};
const filterData = normalizeFilterData(item.filter_data);
const matchesSearch =
!searchTerm ||
meta.title.toLowerCase().includes(searchTerm) ||
meta.summary.toLowerCase().includes(searchTerm);
const matchesGeo = matchesGroup(filterData.geo, state.activeFilters.geo);
const matchesDeal = matchesGroup(filterData.deal, state.activeFilters.deal);
const matchesChannel = matchesGroup(filterData.channel, state.activeFilters.channel);
const matchesMaturity = matchesGroup(filterData.maturity, state.activeFilters.maturity);
const matchesPain = matchesGroup(filterData.pain_point, state.activeFilters.pain_point);
const matchesSolution = matchesGroup(filterData.solution_interest, state.activeFilters.solution_interest);
const matchesAdFree =
!state.activeFilters.ad_free_only || filterData.is_ad_free;
return (
matchesSearch &&
matchesGeo &&
matchesDeal &&
matchesChannel &&
matchesMaturity &&
matchesPain &&
matchesSolution &&
matchesAdFree
);
});
state.filteredStudies = filtered;
state.currentPage = 1;
renderPage();
}
function resetFilters() {
state.activeFilters = {
search: "",
geo: new Set(),
deal: new Set(),
channel: new Set(),
maturity: new Set(),
pain_point: new Set(),
solution_interest: new Set(),
ad_free_only: false
};
elements.searchInput.value = "";
elements.adFreeToggle.checked = false;
document.querySelectorAll("input[type='checkbox']").forEach((box) => {
box.checked = false;
});
document.querySelectorAll(".filter-tag").forEach((tag) => {
tag.classList.remove("active");
tag.setAttribute("aria-pressed", "false");
});
filterData();
}
function updateSetFromCheckbox(event) {
const { name, value, checked } = event.target;
const targetSet = state.activeFilters[name];
if (!targetSet) return;
if (checked) {
targetSet.add(value);
} else {
targetSet.delete(value);
}
filterData();
}
function setupEvents() {
const onSearch = debounce((event) => {
state.activeFilters.search = event.target.value.toLowerCase();
filterData();
}, 150);
elements.searchInput.addEventListener("input", onSearch);
elements.adFreeToggle.addEventListener("change", (event) => {
state.activeFilters.ad_free_only = event.target.checked;
filterData();
});
document.querySelectorAll("input[type='checkbox'][name]").forEach((box) => {
box.addEventListener("change", updateSetFromCheckbox);
});
document.querySelectorAll(".filter-tag").forEach((btn) => {
btn.setAttribute("aria-pressed", "false");
btn.addEventListener("click", () => {
btn.classList.toggle("active");
const value = btn.dataset.value;
const isActive = btn.classList.contains("active");
btn.setAttribute("aria-pressed", String(isActive));
if (isActive) {
state.activeFilters.solution_paths.add(value);
} else {
state.activeFilters.solution_paths.delete(value);
}
filterData();
});
});
elements.resetBtn.addEventListener("click", resetFilters);
}
async function loadData() {
elements.loadError.classList.add("hidden");
try {
const response = await fetch(DATA_URL, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Failed to load: ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Invalid JSON shape");
}
state.caseStudies = data;
state.filteredStudies = data;
renderPage();
} catch (error) {
elements.loadError.classList.remove("hidden");
clearGrid();
setCount(0);
}
}
function init() {
setupEvents();
loadData();
}
init();
})();
// ============================================
// CAROUSEL.JS - Carousel Logic
// ============================================
// Note: Include carousel.js separately if needed
// ============================================
// CASE-STUDY.JS - Detail Page Logic
// ============================================
(() => {
const urlParams = new URLSearchParams(window.location.search);
const currentId = urlParams.get("id") || "cs_001";
const REMOTE_MARKDOWN_BASE_URL = "https://cdn.glowradi.us/case-study-md";
const markdownContainer = document.getElementById("markdown-content");
const tocList = document.getElementById("toc-list");
const GRADIENT_PALETTE = [
"linear-gradient(135deg, #2563eb 0%, #1e40af 100%)",
"linear-gradient(135deg, #059669 0%, #064e3b 100%)",
"linear-gradient(135deg, #7c3aed 0%, #4c1d95 100%)",
"linear-gradient(135deg, #ea580c 0%, #9a3412 100%)"
];
function getGradientIndex(seed = "") {
return Array.from(seed).reduce((hash, char) => hash + char.charCodeAt(0), 0) % GRADIENT_PALETTE.length;
}
function ensureArray(value) {
if (Array.isArray(value)) return value;
if (typeof value === "string" && value.trim()) return [value];
return [];
}
function joinValues(values) {
const items = ensureArray(values);
return items.length ? items.join(", ") : "--";
}
function normalizeStudy(study) {
const filterData = study.filter_data || {};
return {
region: ensureArray(filterData.geo || filterData.region),
deal: ensureArray(filterData.deal || filterData.deal_size),
channel: ensureArray(filterData.channel),
maturity: ensureArray(filterData.maturity || filterData.business_model),
painPoints: ensureArray(filterData.pain_point || filterData.pain_points),
solutionInterest: ensureArray(filterData.solution_interest || filterData.solution_paths)
};
}
function populateMetadata(data) {
const normalized = normalizeStudy(data);
document.getElementById("bread-client").textContent = data.meta.client_name;
document.getElementById("hero-title").textContent = data.meta.title;
document.getElementById("hero-summary").textContent = data.meta.summary;
document.title = data.meta.title;
document.getElementById("meta-timeline").textContent = joinValues(normalized.region);
document.getElementById("meta-deal").textContent = joinValues(normalized.deal);
document.getElementById("meta-region").textContent = joinValues(normalized.channel);
document.getElementById("meta-model").textContent = joinValues(normalized.maturity);
const painContainer = document.getElementById("meta-pain");
painContainer.innerHTML = "";
normalized.painPoints.forEach(p => {
const tag = document.createElement("span");
tag.className = "meta-tag";
tag.textContent = p;
painContainer.appendChild(tag);
});
const solutionContainer = document.getElementById("meta-solution");
solutionContainer.innerHTML = "";
normalized.solutionInterest.forEach(s => {
const tag = document.createElement("span");
tag.className = "meta-tag";
tag.textContent = s;
solutionContainer.appendChild(tag);
});
}
function extractHeadings(content) {
const headingRegex = /^#{2,3}\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
headings.push({
level: match[0].startsWith("###") ? 3 : 2,
text: match[1].trim(),
id: match[1].trim().toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-")
});
}
return headings;
}
function renderTOC(headings) {
if (!tocList) return;
tocList.innerHTML = "";
headings.forEach(h => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = `#${h.id}`;
a.textContent = h.text;
li.appendChild(a);
tocList.appendChild(li);
});
}
function processMarkdown(content) {
const headingRegex = /^#{2,3}\s+(.+)$/gm;
return content.replace(headingRegex, (match, text) => {
const level = match.startsWith("###") ? 3 : 2;
const id = text.trim().toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
return `
Loading...
"; const response = await fetch(`${REMOTE_MARKDOWN_BASE_URL}/${caseStudyId}.md`); if (!response.ok) throw new Error("Markdown not found"); const content = await response.text(); const processed = processMarkdown(content); markdownContainer.innerHTML = marked.parse(processed); const headings = extractHeadings(content); renderTOC(headings); } catch (error) { markdownContainer.innerHTML = "Case study content not available.
"; } } async function loadCaseStudy() { try { const response = await fetch(DATA_URL, { cache: "no-store" }); if (!response.ok) throw new Error("Failed to load"); const data = await response.json(); const study = data.find(s => s.id === currentId); if (!study) { document.getElementById("hero-title").textContent = "Case Study Not Found"; return; } populateMetadata(study); loadMarkdown(currentId); const similarGrid = document.getElementById("similar-grid"); if (similarGrid && study.related_ids) { similarGrid.innerHTML = ""; study.related_ids.forEach(relatedId => { const related = data.find(d => d.id === relatedId); if (related) { const card = document.createElement("div"); card.className = "story-card"; const gradientIndex = getGradientIndex(related.id); card.innerHTML = `