PageSourceSearch

https://iceclubmanager.com/subscription-catalog.js

js iceclubmanager.com collected 2026-10-02 01:31:57 UTC 9,870 bytes, 248 lines download raw bytes

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.