(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ module.exports = { build: { protected_dirs: ["assets", "style", "views", "standard"], default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"], }, }; },{}],2:[function(require,module,exports){ module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, translations_url: "/assets/translations" }; },{}],3:[function(require,module,exports){ "use strict"; /** * A tiny library to handle text translation. */ /** * init parameter object type * @typedef TranslatorParam * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" * * @property {Boolean} use_url_locale_fragment - DEFAULT: true - * If true, the 1st fragment of the window.location url will be parsed as a language code * Example: * window.location * > https://example.com/en/some-page... * then the /en/ part of the url will be taken in priority and locale will be set to "en". * window.location * > https://example.com/some-page... * No locale fragment will be found so locale will not be set from url fragment. * window.location * > https://example.com/some-page/en * Doesn't work, locale fragment must be the first framgment * * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" * The key used to saved the current locale into local storage * * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations * Translations files are expected to be named with their corresponding locale code. * Example: * if supported_languages is set to ["en", "fr", "it"] * and translations_url is set to "https://example.com/translations/"" * Then the expected translations files are * https://example.com/translations/en.json * https://example.com/translations/fr.json * https://example.com/translations/it.json * The json resources must simple key value maps, value being the translated text. */ module.exports = { locale: "en", // ISO 639 lowercase language code supported_languages: ["en"], translations: {}, translations_url: "", use_url_locale_fragment: true, local_storage_key: "translator-prefered-language", /** * Initialize the lib with params * @param {TranslatorParam} params * @returns {Promise} */ init(params) { Object.entries(params).forEach(k_v => { const [key, value] = k_v; if ([ "supported_languages", "use_url_locale_fragment", "local_storage_key", "translations_url" ].includes(key)) { this[key] = value; } }); this.translations_url = this.format_translations_url(this.translations_url); this.supported_languages = this.format_supported_languages(this.supported_languages); return new Promise((resolve, reject) => { const loc = (() => {// Locale from url priority 1 if (this.use_url_locale_fragment) { const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; const fragment_is_locale = this.supported_languages.includes(first_url_fragment); return fragment_is_locale ? first_url_fragment : ""; } else { return ""; } })() || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 || (() => { // locale from navigator priority 3 const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); return this.supported_languages.includes(navigator_locale) ? navigator_locale : this.supported_languages[0] // Default if navigator locale is not supported })(); fetch(`${this.translations_url}${loc}.json`) .then(response => response.json()) .then(response => { this.locale = loc; this.translations = response; resolve(); }) .catch(err => { this.locale = "en"; reject(err); }); }); }, /** * Return a lowercase string without dash. * If given locale is en-EN, then "en" will be returned. * @param {String} locale * @returns A lowercase string */ format_locale(locale) { return locale.split("-")[0].toLowerCase(); }, /** * Appends a slash at the end of the given string if missing, and returns the url. * @param {String} url * @returns {String} */ format_translations_url(url) { if (url.charAt(url.length - 1) !== "/") { url += "/" } return url; }, /** * Return the array of language codes formatted as lowsercase language code. * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. * @param {String[]} languages_codes * @returns {String[]} */ format_supported_languages(languages_codes) { return languages_codes.map(lc => this.format_locale(lc)); }, /** * Fetches a new set of translations in case the wanted language changes * @param {String} locale A lowercase language code * @returns {Promise} */ update_translations(locale) { locale = this.format_locale(locale); return new Promise((resolve, reject) => { fetch(`${this.translations_url}${locale}.json`) .then(response => response.json()) .then(response => { this.translations = response; this.locale = locale; localStorage.setItem(this.local_storage_key, locale); const split_path = window.location.pathname.substring(1).split("/"); const first_url_fragment = split_path[0]; const fragment_is_locale = this.supported_languages.includes(first_url_fragment); if (fragment_is_locale) { split_path.splice(0, 1, locale); const updated_path = split_path.join("/"); window.history.replaceState(null, "", "/" + updated_path); } resolve(); }) .catch(err => { reject(err); }); }); }, /** * Tries to get the translation of the source string, or return the string as it. * @param {String} text The source text to translate * @param {Object} params Some dynamic element to insert in the text * Params can be used if the translated text provide placeholder like {%some_word%} * Example: * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) * -> translation for "Some trad key": "A translated text with {%some_word%}" * -> will be return as : "A translated text with a dynamic parameter" * @returns {String} */ trad: function (text, params = {}) { text = this.translations[text] || text; Object.keys(params).forEach(k => { text = text.replace(`{%${k}%}`, params[k]); }); return text; } }; },{}],4:[function(require,module,exports){ "use strict"; module.exports = { register_key: "objectToHtmlRender", /** * Register "this" as a window scope accessible variable named by the given key, or default. * @param {String} key */ register(key) { const register_key = key || this.register_key; window[register_key] = this; }, /** * This must be called before any other method in order to initialize the lib. * It provides the root of the rendering cycle as a Javascript object. * @param {Object} renderCycleRoot A JS component with a render method. */ setRenderCycleRoot(renderCycleRoot) { this.renderCycleRoot = renderCycleRoot; }, event_name: "objtohtml-render-cycle", /** * Set a custom event name for the event that is trigger on render cycle. * Default is "objtohtml-render-cycle". * @param {String} evt_name */ setEventName(evt_name) { this.event_name = evt_name; }, /** * This is the core agorithm that read an javascript Object and convert it into an HTML element. * @param {Object} obj The object representing the html element must be formatted like: * { * tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li... * xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag. * See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information * style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax, * like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc. * contents: Array or String // This reprensents the contents that will be nested in the created html element. * <div>{contents}</div> * The contents can be an array of other objects reprenting elements (with tag, contents, etc) * or it can be a simple string. * // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title... * // or they can also define custom html5 attributes, like data, my_custom_attr or anything. * } * @returns {HTMLElement} The output html node. */ objectToHtml(obj) { if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process. const objectToHtml = this.objectToHtml.bind(this); const { tag, xmlns } = obj; const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag); const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"]; Object.keys(obj) .filter(attr => !excludeKeys.includes(attr)) .forEach(attr => { switch (attr) { case "class": node.classList.add(...obj[attr].split(" ").filter(s => s !== "")); break; case "on_render": if (!obj.id) { node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`; } if (typeof obj.on_render !== "function") { console.error("The on_render attribute must be a function") } else { this.attach_on_render_callback(node, obj.on_render); } break; default: if (xmlns !== undefined) { node.setAttributeNS(null, attr, obj[attr]) } else { node[attr] = obj[attr]; } } }); if (obj.contents && typeof obj.contents === "string") { node.innerHTML = obj.contents; } else { obj.contents && obj.contents.length > 0 && obj.contents.forEach(el => { switch (typeof el) { case "string": node.innerHTML = el; break; case "object": if (xmlns !== undefined) { el = Object.assign(el, { xmlns }) } node.appendChild(objectToHtml(el)); break; } }); } if (obj.style_rules) { Object.keys(obj.style_rules).forEach(rule => { node.style[rule] = obj.style_rules[rule]; }); } return node; }, on_render_callbacks: [], /** * This is called if the on_render attribute of a component is set. * @param {HTMLElement} node The created html element * @param {Function} callback The callback defined in the js component to render */ attach_on_render_callback(node, callback) { const callback_handler = { callback: e => { if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) { callback(node); const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node))); if (handler_index === -1) { console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.") } else { window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback) this.on_render_callbacks.splice(handler_index, 1); } } }, node, }; const len = this.on_render_callbacks.push(callback_handler); window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback); }, /** * If a main element exists in the html document, it will be used as rendering root. * If not, it will be created and inserted. */ renderCycle: function () { const main_elmt = document.getElementsByTagName("main")[0] || (function () { const created_main = document.createElement("main"); document.body.appendChild(created_main); return created_main; })(); this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" }); }, /** * This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component, * it can start from any component of the tree. The root component must be given as the first argument, the second argument be * be a valid html element in the dom and will be used as the insertion target. * @param {Object} object An object providing a render method returning an object representation of the html to insert * @param {HTMLElement} htmlNode The htlm element to update * @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override", * "insert-before" (must be defined along with an insertIndex key (integer)), * "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove". * In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}... */ subRender(object, htmlNode, options = { mode: "append" }) { let outputNode = null; const get_insert = () => { outputNode = this.objectToHtml(object); return outputNode; }; switch (options.mode) { case "append": htmlNode.appendChild(get_insert()); break; case "override": htmlNode.innerHTML = ""; htmlNode.appendChild(get_insert()); break; case "insert-before": htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]); break; case "adjacent": /** * options.insertLocation must be one of: * * afterbegin * afterend * beforebegin * beforeend */ htmlNode.insertAdjacentHTML(options.insertLocation, get_insert()); break; case "replace": htmlNode.parentNode.replaceChild(get_insert(), htmlNode); break; case "remove": htmlNode.remove(); break; } const evt_name = this.event_name; const event = new CustomEvent(evt_name, { detail: { inputObject: object, outputNode, insertOptions: options, targetNode: htmlNode, } }); window.dispatchEvent(event); }, }; },{}],5:[function(require,module,exports){ "use strict"; const { images_url } = require("../../constants"); class ThemeCard { constructor(props) { this.props = props; } render() { return { tag: "a", class: "theme-card", href: this.props.href, contents: [ { tag: "div", class: "card-img", contents: [{ tag: "img", alt: `thematic image ${this.props.img.replace(/\.[A-Za-z]+/, "")}`, src: `${images_url}/${this.props.img}` }], }, { tag: "div", class: "card-title", contents: [{ tag: "h2", class: "section-title", contents: this.props.title }], }, { tag: "div", class: "card-description", contents: [{ tag: "p", contents: this.props.description }], }, ], }; } } module.exports = ThemeCard; },{"../../constants":2}],6:[function(require,module,exports){ "use strict"; const { images_url } = require("../constants"); const ThemeCard = require("./home-page-components/theme-card"); const WebPage = require("./lib/web-page"); const translator = require("ks-cheap-translator"); const t = translator.trad.bind(translator); class HomePage extends WebPage { constructor() { super({ id: "home-page" }); } render() { return { tag: "div", id: this.id, contents: [ { tag: "div", class: "page-header", contents: [ { tag: "div", class: "big-logo page-contents-center", contents: [ { tag: "img", alt: "logo Kuadrado", src: `${images_url}/logo_kuadrado.svg`, }, { tag: "img", class: "logo-text", alt: "Kuadrado", src: `${images_url}/logo_kuadrado_txt.svg`, }, ], }, { tag: "h1", contents: "Kuadrado Software", class: "page-contents-center" }, { tag: "p", class: "page-contents-center", contents: t("kuadrado-home-description"), }, { tag: "ul", class: "philo-bubbles", contents: [t("Simplicité"), t("Légèreté"), t("Écologie")].map(word => { return { tag: "li", contents: [{ tag: "span", contents: word }], }; }), }, ], }, { tag: "section", class: "page-contents-center poles", contents: [ { title: t("Jeux"), img: "game_controller.svg", href: "/games/", description: t("games-description"), }, { title: t("Pédagogie"), img: "brain.svg", href: "/education/", description: t("education-description"), }, { title: "Software", img: "meca_proc.svg", href: "/software-development/", description: t("software-description"), }, ].map(cardProps => new ThemeCard(cardProps).render()), }, ], }; } } module.exports = HomePage; },{"../constants":2,"./home-page-components/theme-card":5,"./lib/web-page":7,"ks-cheap-translator":3}],7:[function(require,module,exports){ "use strict"; const translator = require("ks-cheap-translator"); const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); if (!this.id) { this.id = "webpage-" + performance.now(); } translator.init({ translations_url, supported_languages: ["fr", "en"], }).then(this.refresh_all.bind(this)); } refresh() { obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) } refresh_all() { obj2htm.renderCycle() } } module.exports = WebPage; },{"../../constants":2,"ks-cheap-translator":3}],8:[function(require,module,exports){ "use strict"; const HomePage = require("./homepage"); const runPage = require("./run-page"); runPage(HomePage); },{"./homepage":6,"./run-page":9}],9:[function(require,module,exports){ "use strict"; const renderer = require("object-to-html-renderer") const Template = require("./template/template"); module.exports = function runPage(PageComponent) { const template = new Template({ page: new PageComponent() }); renderer.register("obj2htm") obj2htm.setRenderCycleRoot(template); obj2htm.renderCycle(); }; },{"./template/template":11,"object-to-html-renderer":4}],10:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); const translator = require("ks-cheap-translator"); const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", }, { url: "/software-development/", text: "Software" } ]; class NavBar { constructor() { this.initEventHandlers(); } handleBurgerClick() { document.getElementById("nav-menu-list").classList.toggle("responsive-show"); } initEventHandlers() { window.addEventListener("click", event => { if ( event.target.id !== "nav-menu-list" && !event.target.classList.contains("burger") && !event.target.parentNode.classList.contains("burger") ) { document.getElementById("nav-menu-list").classList.remove("responsive-show"); } }); } handle_chang_lang(lang) { translator.update_translations(lang).then(() => { obj2htm.renderCycle(); }).catch(err => console.log(err)); } renderHome() { return { tag: "div", class: "home", contents: [ { tag: "a", href: "/", contents: [ { tag: "img", alt: "Logo Kuadrado", src: `${images_url}/logo_kuadrado.svg`, }, { tag: "img", alt: "Kuadrado Software", class: "logo-text", src: `${images_url}/logo_kuadrado_txt.svg`, }, ], }, ], }; } renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") { return { tag: "ul", id: "nav-menu-list", class: isSubmenu ? "submenu" : "", contents: menuItemsArray.map(item => { const { url, text, submenu } = item; const href = `${parentUrl}${url}`; return { tag: "li", class: !isSubmenu && window.location.pathname === href ? "active" : "", contents: [ { tag: "a", href, contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; }).concat({ tag: "li", class: "lang-flags", contents: ["fr", "en"].map(lang => { return { tag: "img", src: `${images_url}/flag-${lang}.svg`, class: translator.locale === lang ? "selected" : "", onclick: this.handle_chang_lang.bind(this, lang) } }) }), }; } renderResponsiveBurger() { return { tag: "div", class: "burger", onclick: this.handleBurgerClick.bind(this), contents: [{ tag: "span", contents: "···" }], }; } render() { return { tag: "nav", contents: [ this.renderHome(), this.renderResponsiveBurger(), this.renderMenu(NAV_MENU_ITEMS), ], }; } } module.exports = NavBar; },{"../../../constants":2,"ks-cheap-translator":3}],11:[function(require,module,exports){ "use strict"; const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); const translator = require("ks-cheap-translator"); const t = translator.trad.bind(translator) class Template { constructor(props) { this.props = props; } render() { return { tag: "main", contents: [ { tag: "header", contents: [new NavBar().render()], }, in_construction && { tag: "section", class: "warning-banner", contents: [ { tag: "strong", class: "page-contents-center", contents: t("Site en construction ..."), }, ], }, { tag: "section", id: "page-container", contents: [this.props.page.render()], }, { tag: "footer", contents: [ { tag: "div", class: "logo", contents: [ { tag: "img", alt: `logo Kuadrado`, src: `${images_url}/logo_kuadrado.svg`, }, { tag: "img", class: "text-logo", alt: "Kuadrado Software", src: `${images_url}/logo_kuadrado_txt.svg`, }, ], }, { tag: "span", contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France", }, { tag: "div", contents: [ { tag: "strong", contents: "<blue>Contact : </blue>" }, { tag: "a", href: "mailto:contact@kuadrado-software.fr", contents: "contact@kuadrado-software.fr", }, ], }, { tag: "div", class: "social", contents: [ { tag: "strong", contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", href: "https://www.linkedin.com/company/kuadrado-software", target: "_blank", contents: "in", title: "Linkedin", }, { tag: "a", href: "https://mastodon.gamedev.place/@kuadrado_software", target: "_blank", contents: "m", title: "Mastodon", } ], }, { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" }, href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md", contents: "OPEN SOURCE" } ] } ], }, ], }; } } module.exports = Template; },{"../../config":1,"../../constants":2,"./components/navbar":10,"ks-cheap-translator":3}]},{},[8]);