1/** 2 * SUBSCRIPTION CATALOG â shared pricing config for the marketing site. 3 * 4 * Loads live pricing + plan limits from GET /api/pricing (the admin-editable 5 * source of truth). Falls back to hardcoded defaults if the fetch fails. 6 * Exposes `window.ICMCatalog`. 7 */ 8(function () { 9 "use strict"; 10 11 var API_BASE = window.SKATING_API_BASE || "https://api.iceclubmanager.com/api"; 12 13 var YEARLY_MONTHS = 10; 14 15 var PLANS = { 16 "petit": { name: "Petit club", members: 30, monthly: 39, yearly: 390 }, 17 "moyen": { name: "Club moyen", members: 80, monthly: 59, yearly: 590 }, 18 "gros": { name: "Gros club", members: 200, monthly: 89, yearly: 890 }, 19 "tres-gros": { name: "Très gros club", members: 500, monthly: 149, yearly: 1490 }, 20 }; 21 22 var MODULES = { 23 "second_section": { label: "2áµ section", i18nKey: "ga_cat_second_section", monthly: { "petit": 15, "moyen": 25, "gros": 40, "tres-gros": 60 } }, 24 "factures": { label: "Factures & cotisations", i18nKey: "ga_mod_invoices", monthly: { "petit": 12, "moyen": 20, "gros": 30, "tres-gros": 45 } }, 25 "evaluations": { label: "Ãvaluations & badges", i18nKey: "ga_mod_evaluations", monthly: { "petit": 8, "moyen": 14, "gros": 22, "tres-gros": 32 } }, 26 "documents": { label: "Documents du club", i18nKey: "ga_mod_documents", monthly: { "petit": 6, "moyen": 10, "gros": 16, "tres-gros": 24 } }, 27 "boutique": { label: "Boutique en ligne", i18nKey: "ga_mod_boutique", monthly: { "petit": 8, "moyen": 14, "gros": 22, "tres-gros": 32 } }, 28 }; 29 30 var PLAN_KEYS = ["petit", "moyen", "gros", "tres-gros"]; 31 32 function catalogT(key, fallback) { 33 return (typeof t === 'function') ? t(key, fallback) : fallback; 34 } 35 36 function translatedPlanName(planKey) { 37 var i18nKey = "ga_plan_" + planKey.replace(/-/g, "_"); 38 return catalogT(i18nKey, PLANS[planKey] ? PLANS[planKey].name : planKey); 39 } 40 41 function translatedModuleLabel(modId) { 42 var m = MODULES[modId]; 43 if (!m) return modId; 44 return m.i18nKey ? catalogT(m.i18nKey, m.label) : m.label; 45 } 46 47 function applyLiveData(data) { 48 if (data.base) { 49 for (var i = 0; i < PLAN_KEYS.length; i++) { 50 var k = PLAN_KEYS[i]; 51 if (data.base[k]) { 52 PLANS[k].monthly = data.base[k].monthly; 53 PLANS[k].yearly = data.base[k].yearly; 54 } 55 } 56 } 57 if (data.planLimits) { 58 for (var i = 0; i < PLAN_KEYS.length; i++) { 59 var k = PLAN_KEYS[i]; 60 if (data.planLimits[k] && data.planLimits[k].maxUsers) { 61 PLANS[k].members = data.planLimits[k].maxUsers; 62 } 63 } 64 } 65 if (data.modules) { 66 for (var modId in data.modules) { 67 if (MODULES[modId]) { 68 for (var j = 0; j < PLAN_KEYS.length; j++) { 69 var pk = PLAN_KEYS[j]; 70 if (data.modules[modId][pk] != null) { 71 MODULES[modId].monthly[pk] = data.modules[modId][pk]; 72 } 73 } 74 } 75 } 76 } 77 if (data.yearlyMonths) { 78 YEARLY_MONTHS = data.yearlyMonths; 79 window.ICMCatalog.YEARLY_MONTHS = YEARLY_MONTHS; 80 } 81 } 82 83 function updatePlanCards() { 84 var cards = document.querySelectorAll(".plan-card[data-plan]"); 85 for (var i = 0; i < cards.length; i++) { 86 var card = cards[i]; 87 var plan = card.getAttribute("data-plan"); 88 var p = PLANS[plan]; 89 if (!p) continue; 90 91 var limitEl = card.querySelector(".plan-card-limit"); 92 if (limitEl) { 93 limitEl.textContent = catalogT('ga_up_to', "Jusqu'à ") + ' ' + p.members + ' ' + catalogT('ga_members_word', 'membres actifs'); 94 } 95 96 var priceEl = card.querySelector(".plan-card-price"); 97 if (priceEl) { 98 if (priceEl.hasAttribute("data-monthly")) { 99 priceEl.setAttribute("data-monthly", p.monthly); 100 priceEl.setAttribute("data-yearly", p.yearly); 101 } else { 102 priceEl.textContent = p.yearly + "â¬"; 103 } 104 } 105 } 106 } 107 108 // Tarifs affiches ailleurs que sur les cartes de formule : textes traduits 109 // (parametres {monthly}/{yearly} via window.ICM_I18N_PARAMS) et donnees 110 // structurees JSON-LD des pages. Les valeurs ecrites dans le HTML ne sont 111 // qu'un repli si /api/pricing ne repond pas. 112 function priceRange() { 113 var monthly = [], yearly = []; 114 for (var i = 0; i < PLAN_KEYS.length; i++) { 115 monthly.push(PLANS[PLAN_KEYS[i]].monthly); 116 yearly.push(PLANS[PLAN_KEYS[i]].yearly); 117 } 118 return { 119 monthlyMin: Math.min.apply(null, monthly), monthlyMax: Math.max.apply(null, monthly), 120 yearlyMin: Math.min.apply(null, yearly), yearlyMax: Math.max.apply(null, yearly), 121 count: PLAN_KEYS.length, 122 }; 123 } 124 125 function formatEuro(n) { 126 return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "\u00a0"); 127 } 128 129 function updateStructuredPricing(r) { 130 var scripts = document.querySelectorAll('script[type="application/ld+json"][data-icm-pricing]'); 131 for (var i = 0; i < scripts.length; i++) { 132 var el = scripts[i]; 133 var data;
134 try { data = JSON.parse(el.textContent); } catch (e) { continue; } 135 var kind = el.getAttribute("data-icm-pricing"); 136 if (kind === "offers" && data.offers) { 137 data.offers.lowPrice = String(r.monthlyMin); 138 data.offers.highPrice = String(r.monthlyMax); 139 data.offers.offerCount = String(r.count); 140 data.offers.description = "Abonnement par club, de " + r.monthlyMin + " \u20ac \u00e0 " + r.monthlyMax + 141 " \u20ac par mois (ou " + formatEuro(r.yearlyMin) + " \u20ac \u00e0 " + formatEuro(r.yearlyMax) + 142 " \u20ac par an) selon le nombre de membres actifs, hors modules optionnels. T\u00e9l\u00e9chargement gratuit ; l'acc\u00e8s des membres est inclus dans l'abonnement du club."; 143 } else if (kind === "faq" && Array.isArray(data.mainEntity)) { 144 // data-icm-faq="<index>:<cle>,..." : la reponse n° index reprend la traduction FR de <cle>. 145 var fr = (typeof TRANSLATIONS !== "undefined" && TRANSLATIONS.fr) ? TRANSLATIONS.fr : null; 146 var map = (el.getAttribute("data-icm-faq") || "").split(","); 147 for (var j = 0; j < map.length; j++) { 148 var parts = map[j].split(":"); 149 var q = data.mainEntity[parseInt(parts[0], 10)]; 150 var key = parts[1]; 151 if (q && q.acceptedAnswer && key && fr && fr[key] && typeof icmFormat === "function") q.acceptedA
151nswer.text = icmFormat(fr[key]); 152 } 153 } 154 el.textContent = JSON.stringify(data, null, 2); 155 } 156 } 157 158 function applyPricingToPage() { 159 var r = priceRange(); 160 window.ICM_I18N_PARAMS = window.ICM_I18N_PARAMS || {}; 161 window.ICM_I18N_PARAMS.monthly = r.monthlyMin; 162 // Meme formule que le prix mensuel affiche : la moins chere au mois. 163 var cheapest = PLAN_KEYS.reduce(function (a, b) { return PLANS[b].monthly < PLANS[a].monthly ? b : a; }); 164 window.ICM_I18N_PARAMS.yearly = formatEuro(PLANS[cheapest].yearly); 165 // Re-rend les textes parametres dans la langue courante (francais compris). 166 if (typeof applyLang === "function" && typeof icmCurrentLang === "function") { 167 var params = document.querySelectorAll("[data-i18n-params]"); 168 if (params.length > 0) applyLang(icmCurrentLang()); 169 } 170 updateStructuredPricing(r); 171 } 172 173 function loadLivePricing() { 174 fetch(API_BASE + "/pricing") 175 .then(function (res) { return res.json(); }) 176 .then(function (data) { 177 applyLiveData(data); 178 updatePlanCards(); 179 applyPricingToPage(); 180 if (typeof window.onICMCatalogReady === "function") { 181 window.onICMCatalogReady(); 182 } 183 }) 184 .catch(function () { 185 // keep hardcoded defaults 186 }); 187 } 188 189 function basePrice(plan, billing) { 190 var p = PLANS[plan]; 191 if (!p) return 0; 192 return billing === "yearly" ? p.yearly : p.monthly; 193 } 194 195 function modulePrice(moduleId, plan, billing) { 196 var m = MODULES[moduleId]; 197 if (!m || m.monthly[plan] == null) return 0; 198 var monthly = m.monthly[plan]; 199 return billing === "yearly" ? monthly * YEARLY_MONTHS : monthly; 200 } 201 202 function activeModules(features) { 203 var mods = []; 204 if (features.leisure && features.competition) mods.push("second_section"); 205 if (features.invoices) mods.push("factures"); 206 if (features.evaluations) mods.push("evaluations"); 207 if (features.documents) mods.push("documents"); 208 if (features.boutique) mods.push("boutique"); 209 return mods; 210 } 211 212 function computeSubscription(plan, billing, features) { 213 var baseLabel = catalogT('ga_cat_base_prefix', 'Socle ') + translatedPlanName(plan); 214 var lines = [{ id: "base", label: baseLabel, amount: basePrice(plan, billing) }]; 215 var mods = activeModules(features); 216 for (var i = 0; i < mods.length; i++) { 217 var id = mods[i]; 218 lines.push({ id: id, label: translatedModuleLabel(id), amount: modulePrice(id, plan, billing) }); 219 } 220 var total = 0; 221 for (var j = 0; j < lines.length; j++) total += lines[j].amount; 222 return { lines: lines, total: total }; 223 } 224 225 function subscriptionTotal(plan, billing, features) { 226 return computeSubscription(plan, billing, features).total; 227 } 228 229 window.ICMCatalog = { 230 YEARLY_MONTHS: YEARLY_MONTHS, 231 PLANS: PLANS, 232 MODULES: MODULES, 233 basePrice: basePrice, 234 modulePrice: modulePrice, 235 activeModules: activeModules, 236 computeSubscription: computeSubscription, 237 subscriptionTotal: subscriptionTotal, 238 loadLivePricing: loadLivePricing, 239 priceRange: priceRange, 240 applyPricingToPage: applyPricingToPage, 241 }; 242 243 // Valeurs par defaut appliquees tout de suite (parametres jamais laisses 244 // bruts), puis remplacees par les tarifs en direct. 245 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", applyPricingToPage); 246 else applyPricingToPage(); 247 loadLivePricing(); 248})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.