<commit_before>'use strict';

/**
 * The presenter class. Stay in the middle between the model (store and AJAX calls) and the view (HTML DOM).
 * @class
 * @params {OrderModel} hModel
 * @params {PDFModel}   hPdfBill
 */
function orderPresenter (hModel) {
    var hMod = hModel,
        nUpdateLoop,
        /** @type {HTMLElement} */
        elMain              = document.getElementsByTagName('main')[0],
        /** @type {HTMLElement} */
        elAside             = document.getElementsByTagName('aside')[0],
        /** @type {HTMLUListElement} */
        elCategoryContainer = document.getElementsByClassName('categories')[0],
        /** @type {HTMLUListElement} */
        elProductsContainer = document.getElementsByClassName('products')[0],
        /** @type {HTMLInputElement} */
        elNameInput         = document.getElementsByClassName('customer-name')[0],
        /** @type {HTMLAnchorElement} */
        elPrintBtn          = document.getElementsByClassName('btn-print-bill')[0],
        /** @type {HTMLAnchorElement} */
        elAlertBtn          = document.getElementsByClassName('btn-ok-alert')[0],
        /** @type {HTMLTableElement} */
        elBillTable         = document.getElementsByClassName('billItems')[0],
        /** @type {HTMLTableCellElement} */
        elBillTotal         = document.getElementsByClassName('billTotal')[0],
        /** @type {HTMLDivElement} */
        elAlertPanel        = document.getElementsByClassName('alert-panel')[0],
        /** @type {HTMLDivElement} */
        elAlertMessage      = document.getElementsByClassName('alert-message')[0],
        /** @type {HTMLDivElement} */
        elMask              = document.getElementsByClassName('mask')[0],
        /** @type {String} */
        sTplBillCategory    = document.getElementsByClassName('billCategoryRow')[0].innerHTML,
        /** @type {String} */
        sTplBillItem        = document.getElementsByClassName('billItemRow')[0].innerHTML,
        /** @type {String} */
        sTplBillSeparator   = document.getElementsByClassName('billSeparatorRow')[0].innerHTML;

    function addEventsListener(elNode, sTypes, fnListener) {
        var aTypes = sTypes.split(' '),
            lenTypes = aTypes.length,
            i;

        for (i = 0; i < lenTypes; i++) {
            elNode.addEventListener(aTypes[i], fnListener);
        }
    }

    /**
     * @private
     */
    function onClickBtnCategory (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            filterCategory(evt.target);
        }
    }

    function onClickBtnProduct (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            orderProduct(evt.target);
        }
    }

    function onClickMenu (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            if (evt.target.classList.contains('add')) {
                incrementProduct(evt.target);
            } else if (evt.target.classList.contains('remove')) {
                decrementProduct(evt.target);
            } else if (evt.target === elPrintBtn) {
                sendBill(evt.target);
            }
        }
    }

    function onClickBtnAlert (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            hideAlert();
        }
    }

    /**
     * Show the alert popup.
     * @param {String}  sText        The text to show into it.
     * @param {Boolean} [bShow=true] If `false` hide the alert.
     */
    function showAlert (sText, bShow) {
        if (bShow === false) {
            hideAlert();
        } else {
            elAlertMessage.innerHTML = sText || '';
            elAlertPanel.classList.remove('hidden');
            elMask.classList.remove('hidden');
        }
    }

    /**
     * Hide the alert popup.
     */
    function hideAlert () {
        elAlertPanel.classList.add('hidden');
        elMask.classList.add('hidden');
    }

    function onWriteName (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'INPUT') {
            enablePrintButton(evt.target.value.length > 2);
        }
    }

    /**
     * Enable the "print bill" button.
     * @param {Boolean} [bEnable=true] `false` to disable it.
     */
    function enablePrintButton (bEnable) {
        if (bEnable === false) {
            elPrintBtn.classList.add('disabled');
        } else {
            elPrintBtn.classList.remove('disabled');
        }
    }

    /**
     * Filter articles via category button.
     * @param {HTMLAnchorElement} elCategoryBtn The category button.
     */
    function filterCategory (elCategoryBtn) {
        var nId = parseInt(elCategoryBtn.dataset.id, 10),
            aBtns = document.getElementsByClassName('products')[0].getElementsByClassName('category-' + nId);

        if (elCategoryBtn.classList.contains('filtered')) {
            elCategoryBtn.classList.remove('filtered');
            $.pif.forEach(aBtns, $.pif.show);
        } else {
            elCategoryBtn.classList.add('filtered');
            $.pif.forEach(aBtns, $.pif.hide);
        }
    }

    /**
     * Order a product via it's button.
     * @param {HTMLAnchorElement} elProductBtn The product button.
     */
    function orderProduct (elProductBtn) {
        var nId    = parseInt(elProductBtn.dataset.id,       10),
            nIdCat = parseInt(elProductBtn.dataset.category, 10);

        hMod.addProduct({
            id       : nId,
            category : nIdCat,
            name     : elProductBtn.innerHTML,
            qty      : 1,
            price    : parseFloat(elProductBtn.dataset.price)
        });
    }

    /**
     * Increment a product amount via it's button.
     * @param {HTMLAnchorElement} elProductBtn The product button.
     */
    function incrementProduct (elProductBtn) {
        var nId = parseInt(elProductBtn.dataset.id, 10);

        hMod.incrementProduct({
            id  : nId,
            qty : 1
        });
    }

    function decrementProduct (elButton) {
        var nId = parseInt(elButton.dataset.id, 10);

        hMod.decrementProduct({
            id  : nId,
            qty : 1
        });
    }

    /**
     * Add the items to the right bill container.
     *
     * @param {object} hBill The bill data coming from the model.
     * @param {object} hBill.items An object containing the items as a value and their id as a key.
     * @param {number} hBill.total The total amount of the whole bill.
     */
    function addToBill (hBill) {
        elBillTable.innerHTML = '';
        var sHTMLRow,
            hCat = hModel.getCategories(),
            i,
            hItem,
            elTr,
            nLastCategory,
            aOrderedBill = orderBillByCategories(hBill.items),
            billLen = aOrderedBill.length,
            elSeparator = document.createElement('tr');

        elSeparator.innerHTML =  riot.render(sTplBillSeparator);

        for (i = 0; i < billLen; i++) {
            hItem = aOrderedBill[i];
            if (nLastCategory !== hItem.category) {
                if (i > 0) {
                    elBillTable.appendChild(elSeparator.cloneNode(true));
                }
                if (hCat[hItem.category]) {
                    elTr = document.createElement('tr');
                    elTr.innerHTML = riot.render(sTplBillCategory, {
                        name : hCat[hItem.category].name
                    });
                    elBillTable.appendChild(elTr);
                }
            }

            sHTMLRow = riot.render(sTplBillItem, {
                id     : hItem.id,
                name   : hItem.name,
                amount : hItem.qty,
                price  : $.pif.formatPrice(hItem.qty * hItem.price)
            });
            elTr = document.createElement('tr');

            elTr.innerHTML = sHTMLRow;
            elBillTable.appendChild(elTr);

            nLastCategory = hItem.category
        }

        elBillTable.appendChild(elSeparator);

        // Total
        elBillTotal.innerHTML = $.pif.formatPrice(hBill.total) + ' €';
    }

    /**
     * Group the bill items by category.
     *
     * @return object[] The items grouped by category.
     */
    function orderBillByCategories (hItems) {
        var aResults = [],
            nId,
            aCat = hModel.getCategories();

        for (nId in hItems) {
            aResults.push(hItems[nId]);
        }
        aResults.sort(function (hItem1, hItem2) {
            if (!aCat[hItem1.category] || !aCat[hItem2.category]) {
                return 1;
            }
            var nPriority1 = aCat[hItem1.category].priority,
                nPriority2 = aCat[hItem2.category].priority,
                nReturn = 0;

            if (nPriority1 < nPriority2) {
                nReturn = -1;
            } else if (nPriority1 > nPriority2) {
                nReturn = 1;
            }

            return nReturn;
        });

        return aResults;
    }

    /**
     * Validate the server response after the bill is sent to it.
     * @param {Object} hResponse The response object.
     * @param {Object} hResponse.errors      An object of articles with errors formatted as `{"Article_name" : max_quantity, ...}`.
     * @param {Number} hResponse.bill_id     The bill ID.
     * @param {String} hResponse.customer_id The customer ID.
     * @param {String} hResponse.date        The bill timestamp.
     * @param {Number} hResponse.total       The validated-by-server bill total.
     * @return {Boolean} `true` if everything is fine, `false` and print an error otherwise.
     */
    function validateBillResponse (hResponse) {
        // Response missing
        if (!hResponse) {
            // Items missing
            showAlert("
Il server non ha risposto correttamente
Ritenta o chiama un tecnico");
            return false;
        }
        if (!hResponse.bill_id) {
            var sName,
                aTxtSupply = [];

            for (sName in hResponse.errors) {
                aTxtSupply.push(hResponse.errors[sName] + ' ' + sName);
            }

            showAlert("
Disponibilità non sufficienti
È rimasto " + aTxtSupply.join('; '));
            return false;
        }
        // Total amount changed
        if (parseFloat(hResponse.total) !== hMod.getTotal()) {
            showAlert("
Totale variato
Il totale aggiornato è di " + $.pif.formatPrice(parseFloat(hResponse.total)) + " €");
            return false;
        }
        return true;
    }

    /**
     * Send the bill to the server and validate the response.
     * @param elBtn
     */
    function sendBill (elBtn) {
        if (elBtn.classList.contains('disabled') || hModel.billIsEmpty()) {
            return;
        }
        /**
         * Function that handle the AJAX response.
         * @param {Object} hResponse The response object.
         * @param {Object} hResponse.errors      An object of articles with errors formatted as `{"Article_name" : max_quantity, ...}`.
         * @param {Number} hResponse.bill_id     The bill ID.
         * @param {String} hResponse.customer_id The customer ID.
         * @param {String} hResponse.date        The bill timestamp.
         * @param {Number} hResponse.total       The validated-by-server bill total.
         */
        var fnAjaxSuccess = function (hResponse) {
                if (validateBillResponse(hResponse)) {
                    // @todo Print the PDF bill with hResponse.customer_id and hResponse.bill_id
                }
            };

        hMod.commitBill(elNameInput.value, fnAjaxSuccess, function (nStatus) {
            showAlert("
Errore di comunicazione col server
Ritenta o chiama un tecnico");
        });
    }

    function disableEvent (evt) {
        evt.preventDefault();
    }

    function getCategories () {
        var hCat = {};
        $.pif.forEach(elCategoryContainer.children, function (elListItem) {
            var elButton = elListItem.getElementsByTagName('A')[0],
                nId = parseInt(elButton.dataset.id, 10);
            hCat[nId] = {
                id       : nId,
                name     : elButton.innerHTML,
                priority : parseInt(elButton.dataset.priority, 10)
            };
        });

        return hCat;
    }

    function setUploadLoop () {
        return setInterval(function () {
            hMod.getUpdates();
        }, 1000 * 1);
    }

    function refreshButtons (hItems) {
        var sName,
            aValues,
            nQty,
            elButton;
        for (sName in hItems) {
            aValues = hItems[sName];
            // aValues[0] = quantity, aValues[1] = price
            nQty = aValues[0];
            elButton = document.querySelector("[data-name='" + sName +"']");
            // @todo Find the button
            if (nQty !== null && nQty <= 5) {
                elButton.classList.add('badge');
                elButton.dataset.badge = nQty;
            } else {
                elButton.classList.remove('badge');
                delete elButton.dataset.badge
            }
        }
    }

    hMod.on('addToBill', addToBill);
    hMod.on('refreshButtons', refreshButtons);

    addEventsListener(elCategoryContainer, 'click touch', onClickBtnCategory);
    addEventsListener(elProductsContainer, 'click touch', onClickBtnProduct);
    addEventsListener(elAside,             'click touch', onClickMenu);
    addEventsListener(elNameInput,         'keyup',       onWriteName);
    addEventsListener(elMain,              'dragstart',   disableEvent);
    addEventsListener(elAlertBtn,          'click touch', onClickBtnAlert);

    hMod.setCategories(getCategories());
    nUpdateLoop = setUploadLoop();
}

orderPresenter(new OrderModel());<commit_msg>Added printing<commit_after>'use strict';

/**
 * The presenter class. Stay in the middle between the model (store and AJAX calls) and the view (HTML DOM).
 * @class
 * @params {OrderModel} hModel
 * @params {PDFModel}   hPdfBill
 */
function orderPresenter (hModel) {
    var hMod = hModel,
        nUpdateLoop,
        /** @type {HTMLElement} */
        elMain              = document.getElementsByTagName('main')[0],
        /** @type {HTMLElement} */
        elAside             = document.getElementsByTagName('aside')[0],
        /** @type {HTMLUListElement} */
        elCategoryContainer = document.getElementsByClassName('categories')[0],
        /** @type {HTMLUListElement} */
        elProductsContainer = document.getElementsByClassName('products')[0],
        /** @type {HTMLInputElement} */
        elNameInput         = document.getElementsByClassName('customer-name')[0],
        /** @type {HTMLAnchorElement} */
        elPrintBtn          = document.getElementsByClassName('btn-print-bill')[0],
        /** @type {HTMLAnchorElement} */
        elAlertBtn          = document.getElementsByClassName('btn-ok-alert')[0],
        /** @type {HTMLTableElement} */
        elBillTable         = document.getElementsByClassName('billItems')[0],
        /** @type {HTMLTableCellElement} */
        elBillTotal         = document.getElementsByClassName('billTotal')[0],
        /** @type {HTMLDivElement} */
        elAlertPanel        = document.getElementsByClassName('alert-panel')[0],
        /** @type {HTMLDivElement} */
        elAlertMessage      = document.getElementsByClassName('alert-message')[0],
        /** @type {HTMLDivElement} */
        elMask              = document.getElementsByClassName('mask')[0],
        /** @type {String} */
        sTplBillCategory    = document.getElementsByClassName('billCategoryRow')[0].innerHTML,
        /** @type {String} */
        sTplBillItem        = document.getElementsByClassName('billItemRow')[0].innerHTML,
        /** @type {String} */
        sTplBillSeparator   = document.getElementsByClassName('billSeparatorRow')[0].innerHTML;

    function addEventsListener(elNode, sTypes, fnListener) {
        var aTypes = sTypes.split(' '),
            lenTypes = aTypes.length,
            i;

        for (i = 0; i < lenTypes; i++) {
            elNode.addEventListener(aTypes[i], fnListener);
        }
    }

    /**
     * @private
     */
    function onClickBtnCategory (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            filterCategory(evt.target);
        }
    }

    function onClickBtnProduct (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            orderProduct(evt.target);
        }
    }

    function onClickMenu (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            if (evt.target.classList.contains('add')) {
                incrementProduct(evt.target);
            } else if (evt.target.classList.contains('remove')) {
                decrementProduct(evt.target);
            } else if (evt.target === elPrintBtn) {
                sendBill(evt.target);
            }
        }
    }

    function onClickBtnAlert (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'A') {
            hideAlert();
        }
    }

    /**
     * Show the alert popup.
     * @param {String}  sText        The text to show into it.
     * @param {Boolean} [bShow=true] If `false` hide the alert.
     */
    function showAlert (sText, bShow) {
        if (bShow === false) {
            hideAlert();
        } else {
            elAlertMessage.innerHTML = sText || '';
            elAlertPanel.classList.remove('hidden');
            elMask.classList.remove('hidden');
        }
    }

    /**
     * Hide the alert popup.
     */
    function hideAlert () {
        elAlertPanel.classList.add('hidden');
        elMask.classList.add('hidden');
    }

    function onWriteName (evt) {
        evt.preventDefault();
        if (evt.target.tagName === 'INPUT') {
            enablePrintButton(evt.target.value.length > 2);
        }
    }

    /**
     * Enable the "print bill" button.
     * @param {Boolean} [bEnable=true] `false` to disable it.
     */
    function enablePrintButton (bEnable) {
        if (bEnable === false) {
            elPrintBtn.classList.add('disabled');
        } else {
            elPrintBtn.classList.remove('disabled');
        }
    }

    /**
     * Filter articles via category button.
     * @param {HTMLAnchorElement} elCategoryBtn The category button.
     */
    function filterCategory (elCategoryBtn) {
        var nId = parseInt(elCategoryBtn.dataset.id, 10),
            aBtns = document.getElementsByClassName('products')[0].getElementsByClassName('category-' + nId);

        if (elCategoryBtn.classList.contains('filtered')) {
            elCategoryBtn.classList.remove('filtered');
            $.pif.forEach(aBtns, $.pif.show);
        } else {
            elCategoryBtn.classList.add('filtered');
            $.pif.forEach(aBtns, $.pif.hide);
        }
    }

    /**
     * Order a product via it's button.
     * @param {HTMLAnchorElement} elProductBtn The product button.
     */
    function orderProduct (elProductBtn) {
        var nId    = parseInt(elProductBtn.dataset.id,       10),
            nIdCat = parseInt(elProductBtn.dataset.category, 10);

        hMod.addProduct({
            id       : nId,
            category : nIdCat,
            name     : elProductBtn.innerHTML,
            qty      : 1,
            price    : parseFloat(elProductBtn.dataset.price)
        });
    }

    /**
     * Increment a product amount via it's button.
     * @param {HTMLAnchorElement} elProductBtn The product button.
     */
    function incrementProduct (elProductBtn) {
        var nId = parseInt(elProductBtn.dataset.id, 10);

        hMod.incrementProduct({
            id  : nId,
            qty : 1
        });
    }

    function decrementProduct (elButton) {
        var nId = parseInt(elButton.dataset.id, 10);

        hMod.decrementProduct({
            id  : nId,
            qty : 1
        });
    }

    /**
     * Add the items to the right bill container.
     *
     * @param {object} hBill The bill data coming from the model.
     * @param {object} hBill.items An object containing the items as a value and their id as a key.
     * @param {number} hBill.total The total amount of the whole bill.
     */
    function addToBill (hBill) {
        elBillTable.innerHTML = '';
        var sHTMLRow,
            hCat = hModel.getCategories(),
            i,
            hItem,
            elTr,
            nLastCategory,
            aOrderedBill = orderBillByCategories(hBill.items),
            billLen = aOrderedBill.length,
            elSeparator = document.createElement('tr');

        elSeparator.innerHTML =  riot.render(sTplBillSeparator);

        for (i = 0; i < billLen; i++) {
            hItem = aOrderedBill[i];
            if (nLastCategory !== hItem.category) {
                if (i > 0) {
                    elBillTable.appendChild(elSeparator.cloneNode(true));
                }
                if (hCat[hItem.category]) {
                    elTr = document.createElement('tr');
                    elTr.innerHTML = riot.render(sTplBillCategory, {
                        name : hCat[hItem.category].name
                    });
                    elBillTable.appendChild(elTr);
                }
            }

            sHTMLRow = riot.render(sTplBillItem, {
                id     : hItem.id,
                name   : hItem.name,
                amount : hItem.qty,
                price  : $.pif.formatPrice(hItem.qty * hItem.price)
            });
            elTr = document.createElement('tr');

            elTr.innerHTML = sHTMLRow;
            elBillTable.appendChild(elTr);

            nLastCategory = hItem.category
        }

        elBillTable.appendChild(elSeparator);

        // Total
        elBillTotal.innerHTML = $.pif.formatPrice(hBill.total) + ' €';
    }

    /**
     * Group the bill items by category.
     *
     * @return object[] The items grouped by category.
     */
    function orderBillByCategories (hItems) {
        var aResults = [],
            nId,
            aCat = hModel.getCategories();

        for (nId in hItems) {
            aResults.push(hItems[nId]);
        }
        aResults.sort(function (hItem1, hItem2) {
            if (!aCat[hItem1.category] || !aCat[hItem2.category]) {
                return 1;
            }
            var nPriority1 = aCat[hItem1.category].priority,
                nPriority2 = aCat[hItem2.category].priority,
                nReturn = 0;

            if (nPriority1 < nPriority2) {
                nReturn = -1;
            } else if (nPriority1 > nPriority2) {
                nReturn = 1;
            }

            return nReturn;
        });

        return aResults;
    }

    /**
     * Validate the server response after the bill is sent to it.
     * @param {Object} hResponse The response object.
     * @param {Object} hResponse.errors      An object of articles with errors formatted as `{"Article_name" : max_quantity, ...}`.
     * @param {Number} hResponse.bill_id     The bill ID.
     * @param {String} hResponse.customer_id The customer ID.
     * @param {String} hResponse.date        The bill timestamp.
     * @param {Number} hResponse.total       The validated-by-server bill total.
     * @return {Boolean} `true` if everything is fine, `false` and print an error otherwise.
     */
    function validateBillResponse (hResponse) {
        // Response missing
        if (!hResponse) {
            // Items missing
            showAlert("
Il server non ha risposto correttamente
Ritenta o chiama un tecnico");
            return false;
        }
        if (!hResponse.bill_id) {
            var sName,
                aTxtSupply = [];

            for (sName in hResponse.errors) {
                aTxtSupply.push(hResponse.errors[sName] + ' ' + sName);
            }

            showAlert("
Disponibilità non sufficienti
È rimasto " + aTxtSupply.join('; '));
            return false;
        }
        // Total amount changed
        if (parseFloat(hResponse.total) !== hMod.getTotal()) {
            showAlert("
Totale variato
Il totale aggiornato è di " + $.pif.formatPrice(parseFloat(hResponse.total)) + " €");
            return false;
        }
        return true;
    }

    /**
     * Send the bill to the server and validate the response.
     * @param elBtn
     */
    function sendBill (elBtn) {
        if (elBtn.classList.contains('disabled') || hModel.billIsEmpty()) {
            return;
        }
        /**
         * Function that handle the AJAX response.
         * @param {Object} hResponse The response object.
         * @param {Object} hResponse.errors      An object of articles with errors formatted as `{"Article_name" : max_quantity, ...}`.
         * @param {Number} hResponse.bill_id     The bill ID.
         * @param {String} hResponse.customer_id The customer ID.
         * @param {String} hResponse.date        The bill timestamp.
         * @param {Number} hResponse.total       The validated-by-server bill total.
         * @param {String} hResponse.pdf_url     The PDF url to print it.
         */
        var fnAjaxSuccess = function (hResponse) {
                if (validateBillResponse(hResponse)) {
                    var hPrintWindow = window.open(hResponse.pdf_url),
                        isFirefox = typeof InstallTrigger !== 'undefined';   // Firefox 1.0+
                    if (!isFirefox) {// Firefox PDF.js is buggy. Better to not trigger automatic print
                        hPrintWindow.print();
                    }
                    window.location.reload();
                }
            };

        hMod.commitBill(elNameInput.value, fnAjaxSuccess, function (nStatus) {
            showAlert("
Errore di comunicazione col server
Ritenta o chiama un tecnico");
        });
    }

    function disableEvent (evt) {
        evt.preventDefault();
    }

    function getCategories () {
        var hCat = {};
        $.pif.forEach(elCategoryContainer.children, function (elListItem) {
            var elButton = elListItem.getElementsByTagName('A')[0],
                nId = parseInt(elButton.dataset.id, 10);
            hCat[nId] = {
                id       : nId,
                name     : elButton.innerHTML,
                priority : parseInt(elButton.dataset.priority, 10)
            };
        });

        return hCat;
    }

    function setUploadLoop () {
        return setInterval(function () {
            hMod.getUpdates();
        }, 1000 * 1);
    }

    function refreshButtons (hItems) {
        var sName,
            aValues,
            nQty,
            elButton;
        for (sName in hItems) {
            aValues = hItems[sName];
            // aValues[0] = quantity, aValues[1] = price
            nQty = aValues[0];
            elButton = document.querySelector("[data-name='" + sName +"']");
            // @todo Find the button
            if (nQty !== null && nQty <= 5) {
                elButton.classList.add('badge');
                elButton.dataset.badge = nQty;
            } else {
                elButton.classList.remove('badge');
                delete elButton.dataset.badge
            }
        }
    }

    hMod.on('addToBill', addToBill);
    hMod.on('refreshButtons', refreshButtons);

    addEventsListener(elCategoryContainer, 'click touch', onClickBtnCategory);
    addEventsListener(elProductsContainer, 'click touch', onClickBtnProduct);
    addEventsListener(elAside,             'click touch', onClickMenu);
    addEventsListener(elNameInput,         'keyup',       onWriteName);
    addEventsListener(elMain,              'dragstart',   disableEvent);
    addEventsListener(elAlertBtn,          'click touch', onClickBtnAlert);

    hMod.setCategories(getCategories());
    nUpdateLoop = setUploadLoop();
}

orderPresenter(new OrderModel());<|endoftext|>


<commit_before>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Colour input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldColour');

goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Field');

goog.require('goog.math.Size');
goog.require('goog.color');

/**
 * Class for a colour input field.
 * @param {string=} opt_value The initial value of the field. Should be in
 *    '#rrggbb' format. Defaults to the first value in the default colour array.
 * @param {Function=} opt_validator A function that is called to validate
 *    changes to the field's value. Takes in a colour string & returns a
 *    validated colour string ('#rrggbb' format), or null to abort the change.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldColour = function(opt_value, opt_validator) {
  opt_value = this.doClassValidation_(opt_value);
  if (opt_value === null) {
    opt_value = Blockly.FieldColour.COLOURS[0];
  }
  Blockly.FieldColour.superClass_.constructor.call(
      this, opt_value, opt_validator);
};
goog.inherits(Blockly.FieldColour, Blockly.Field);

/**
 * Construct a FieldColour from a JSON arg object.
 * @param {!Object} options A JSON object with options (colour).
 * @return {!Blockly.FieldColour} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldColour.fromJson = function(options) {
  return new Blockly.FieldColour(options['colour']);
};

/**
 * Default width of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_WIDTH = 16;

/**
 * Default height of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_HEIGHT = 12;

/**
 * Regex that defines the form of a colour string.
 * @type {RegExp}
 */
Blockly.FieldColour.COLOUR_REGEX = new RegExp('#[0-9a-fA-F]{6}');

/**
 * Serializable fields are saved by the XML renderer, non-serializable fields
 * are not. Editable fields should also be serializable.
 * @type {boolean}
 * @const
 */
Blockly.FieldColour.prototype.SERIALIZABLE = true;

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldColour.prototype.CURSOR = 'default';

/**
 * Used to tell if the field needs to be rendered the next time the block is
 * rendered. Colour fields are statically sized, and only need to be
 * rendered at initialization.
 * @type {boolean}
 * @private
 */
Blockly.FieldColour.prototype.isDirty_ = false;

/**
 * Array of colours used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.colours_ = null;

/**
 * Array of colour tooltips used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.titles_ = null;

/**
 * Number of colour columns used by this field.  If 0, use the global setting.
 * By default use the global constants for columns.
 * @type {number}
 * @private
 */
Blockly.FieldColour.prototype.columns_ = 0;

/**
 * Border colour for the dropdown div showing the colour picker.  Must be a CSS
 * string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BORDER_COLOUR = 'silver';

/**
 * Background colour for the dropdown div showing the colour picker.  Must be a
 * CSS string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white';

/**
 * Create the block UI for this colour field.
 * @package
 */
Blockly.FieldColour.prototype.initView = function() {
  this.size_ = new goog.math.Size(Blockly.FieldColour.DEFAULT_WIDTH,
      Blockly.FieldColour.DEFAULT_HEIGHT);
  this.createBorderRect_();
  this.borderRect_.style['fillOpacity'] = 1;
  this.borderRect_.style.fill = this.value_;
};

/**
 * Close the colour picker if this input is being deleted.
 */
Blockly.FieldColour.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldColour.superClass_.dispose.call(this);
};

/**
 * Ensure that the input value is a valid colour.
 * @param {string=} newValue The input value.
 * @return {?string} A valid colour, or null if invalid.
 * @protected
 */
Blockly.FieldColour.prototype.doClassValidation_ = function(newValue) {
  if (typeof newValue != 'string') {
    return null;
  }
  if (goog.color.isValidColor(newValue)) {
    return goog.color.parse(newValue).hex;
  }
  return null;
};

/**
 * Update the value of this colour field, and update the displayed colour.
 * @param {string} newValue The new colour in '#rrggbb' format.
 * @protected
 */
Blockly.FieldColour.prototype.doValueUpdate_ = function(newValue) {
  this.value_ = newValue;
  if (this.borderRect_) {
    this.borderRect_.style.fill = newValue;
  }
};

/**
 * Get the text for this field.  Used when the block is collapsed.
 * @return {string} Text representing the value of this field.
 */
Blockly.FieldColour.prototype.getText = function() {
  var colour = this.value_;
  // Try to use #rgb format if possible, rather than #rrggbb.
  var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
  if (m) {
    colour = '#' + m[1] + m[2] + m[3];
  }
  return colour;
};

/**
 * An array of colour strings for the palette.
 * Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.COLOURS = [
  // grays
  '#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
  // reds
  '#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
  // oranges
  '#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
  // yellows
  '#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
  // olives
  '#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
  // greens
  '#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
  // turquoises
  '#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
  // blues
  '#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
  // purples
  '#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
  // violets
  '#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
];

/**
 * An array of tooltip strings for the palette.  If not the same length as
 * COLOURS, the colour's hex code will be used for any missing titles.
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.TITLES = [];

/**
 * Number of columns in the palette.
 * All colour pickers use this unless overridden with setColumns.
 */
Blockly.FieldColour.COLUMNS = 7;

/**
 * Set a custom colour grid for this field.
 * @param {Array.<string>} colours Array of colours for this block,
 *     or null to use default (Blockly.FieldColour.COLOURS).
 * @param {Array.<string>} opt_titles Optional array of colour tooltips,
 *     or null to use default (Blockly.FieldColour.TITLES).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColours = function(colours, opt_titles) {
  this.colours_ = colours;
  if (opt_titles !== undefined) {
    this.titles_ = opt_titles;
  }
  return this;
};

/**
 * Set a custom grid size for this field.
 * @param {number} columns Number of columns for this block,
 *     or 0 to use default (Blockly.FieldColour.COLUMNS).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColumns = function(columns) {
  this.columns_ = columns;
  return this;
};

/**
 * Create a palette under the colour field.
 * @private
 */
Blockly.FieldColour.prototype.showEditor_ = function() {
  var picker = this.createWidget_();
  Blockly.DropDownDiv.getContentDiv().appendChild(picker);
  Blockly.DropDownDiv.setColour(
      this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR);

  Blockly.DropDownDiv.showPositionedByField(this);

  // Configure event handler on the table to listen for any event in a cell.
  Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker,
      'mouseup', this, this.onClick_);
};

/**
 * Handle a click on a colour cell.
 * @param {!Event} e Mouse event.
 * @private
 */
Blockly.FieldColour.prototype.onClick_ = function(e) {
  var cell = e.target;
  if (cell && !cell.label) {
    // The target element is the 'div', back out to the 'td'.
    cell = cell.parentNode;
  }
  var colour = cell && cell.label;
  Blockly.WidgetDiv.hide();
  if (this.sourceBlock_) {
    // Call any validation function, and allow it to override.
    colour = this.callValidator(colour);
  }
  if (colour !== null) {
    this.setValue(colour);
  }
};

/**
 * Create a colour picker widget.
 * @return {!Element} The newly created colour picker.
 * @private
 */
Blockly.FieldColour.prototype.createWidget_ = function() {
  var columns = this.columns_ || Blockly.FieldColour.COLUMNS;
  var colours = this.colours_ || Blockly.FieldColour.COLOURS;
  var titles = this.titles_ || Blockly.FieldColour.TITLES;
  var selectedColour = this.getValue();
  // Create the palette.
  var table = document.createElement('table');
  table.className = 'blocklyColourTable';
  var row;
  for (var i = 0; i < colours.length; i++) {
    if (i % columns == 0) {
      row = document.createElement('tr');
      table.appendChild(row);
    }
    var cell = document.createElement('td');
    row.appendChild(cell);
    var div = document.createElement('div');
    cell.appendChild(div);
    cell.label = colours[i];  // This becomes the value, if clicked.
    cell.title = titles[i] || colours[i];
    div.style.backgroundColor = colours[i];
    if (colours[i] == selectedColour) {
      div.className = 'blocklyColourSelected';
    }
  }
  return table;
};

/**
 * Hide the colour picker widget.
 * @private
 */
Blockly.FieldColour.widgetDispose_ = function() {
  if (Blockly.FieldColour.onUpWrapper_) {
    Blockly.unbindEvent_(Blockly.FieldColour.onUpWrapper_);
  }
  Blockly.Events.setGroup(false);
};

Blockly.Field.register('field_colour', Blockly.FieldColour);
<commit_msg>Remove orphaned regexp.<commit_after>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Colour input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldColour');

goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Field');

goog.require('goog.math.Size');
goog.require('goog.color');

/**
 * Class for a colour input field.
 * @param {string=} opt_value The initial value of the field. Should be in
 *    '#rrggbb' format. Defaults to the first value in the default colour array.
 * @param {Function=} opt_validator A function that is called to validate
 *    changes to the field's value. Takes in a colour string & returns a
 *    validated colour string ('#rrggbb' format), or null to abort the change.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldColour = function(opt_value, opt_validator) {
  opt_value = this.doClassValidation_(opt_value);
  if (opt_value === null) {
    opt_value = Blockly.FieldColour.COLOURS[0];
  }
  Blockly.FieldColour.superClass_.constructor.call(
      this, opt_value, opt_validator);
};
goog.inherits(Blockly.FieldColour, Blockly.Field);

/**
 * Construct a FieldColour from a JSON arg object.
 * @param {!Object} options A JSON object with options (colour).
 * @return {!Blockly.FieldColour} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldColour.fromJson = function(options) {
  return new Blockly.FieldColour(options['colour']);
};

/**
 * Default width of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_WIDTH = 16;

/**
 * Default height of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_HEIGHT = 12;

/**
 * Serializable fields are saved by the XML renderer, non-serializable fields
 * are not. Editable fields should also be serializable.
 * @type {boolean}
 * @const
 */
Blockly.FieldColour.prototype.SERIALIZABLE = true;

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldColour.prototype.CURSOR = 'default';

/**
 * Used to tell if the field needs to be rendered the next time the block is
 * rendered. Colour fields are statically sized, and only need to be
 * rendered at initialization.
 * @type {boolean}
 * @private
 */
Blockly.FieldColour.prototype.isDirty_ = false;

/**
 * Array of colours used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.colours_ = null;

/**
 * Array of colour tooltips used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.titles_ = null;

/**
 * Number of colour columns used by this field.  If 0, use the global setting.
 * By default use the global constants for columns.
 * @type {number}
 * @private
 */
Blockly.FieldColour.prototype.columns_ = 0;

/**
 * Border colour for the dropdown div showing the colour picker.  Must be a CSS
 * string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BORDER_COLOUR = 'silver';

/**
 * Background colour for the dropdown div showing the colour picker.  Must be a
 * CSS string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white';

/**
 * Create the block UI for this colour field.
 * @package
 */
Blockly.FieldColour.prototype.initView = function() {
  this.size_ = new goog.math.Size(Blockly.FieldColour.DEFAULT_WIDTH,
      Blockly.FieldColour.DEFAULT_HEIGHT);
  this.createBorderRect_();
  this.borderRect_.style['fillOpacity'] = 1;
  this.borderRect_.style.fill = this.value_;
};

/**
 * Close the colour picker if this input is being deleted.
 */
Blockly.FieldColour.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldColour.superClass_.dispose.call(this);
};

/**
 * Ensure that the input value is a valid colour.
 * @param {string=} newValue The input value.
 * @return {?string} A valid colour, or null if invalid.
 * @protected
 */
Blockly.FieldColour.prototype.doClassValidation_ = function(newValue) {
  if (typeof newValue != 'string') {
    return null;
  }
  if (goog.color.isValidColor(newValue)) {
    return goog.color.parse(newValue).hex;
  }
  return null;
};

/**
 * Update the value of this colour field, and update the displayed colour.
 * @param {string} newValue The new colour in '#rrggbb' format.
 * @protected
 */
Blockly.FieldColour.prototype.doValueUpdate_ = function(newValue) {
  this.value_ = newValue;
  if (this.borderRect_) {
    this.borderRect_.style.fill = newValue;
  }
};

/**
 * Get the text for this field.  Used when the block is collapsed.
 * @return {string} Text representing the value of this field.
 */
Blockly.FieldColour.prototype.getText = function() {
  var colour = this.value_;
  // Try to use #rgb format if possible, rather than #rrggbb.
  var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
  if (m) {
    colour = '#' + m[1] + m[2] + m[3];
  }
  return colour;
};

/**
 * An array of colour strings for the palette.
 * Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.COLOURS = [
  // grays
  '#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
  // reds
  '#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
  // oranges
  '#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
  // yellows
  '#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
  // olives
  '#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
  // greens
  '#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
  // turquoises
  '#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
  // blues
  '#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
  // purples
  '#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
  // violets
  '#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
];

/**
 * An array of tooltip strings for the palette.  If not the same length as
 * COLOURS, the colour's hex code will be used for any missing titles.
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.TITLES = [];

/**
 * Number of columns in the palette.
 * All colour pickers use this unless overridden with setColumns.
 */
Blockly.FieldColour.COLUMNS = 7;

/**
 * Set a custom colour grid for this field.
 * @param {Array.<string>} colours Array of colours for this block,
 *     or null to use default (Blockly.FieldColour.COLOURS).
 * @param {Array.<string>} opt_titles Optional array of colour tooltips,
 *     or null to use default (Blockly.FieldColour.TITLES).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColours = function(colours, opt_titles) {
  this.colours_ = colours;
  if (opt_titles !== undefined) {
    this.titles_ = opt_titles;
  }
  return this;
};

/**
 * Set a custom grid size for this field.
 * @param {number} columns Number of columns for this block,
 *     or 0 to use default (Blockly.FieldColour.COLUMNS).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColumns = function(columns) {
  this.columns_ = columns;
  return this;
};

/**
 * Create a palette under the colour field.
 * @private
 */
Blockly.FieldColour.prototype.showEditor_ = function() {
  var picker = this.createWidget_();
  Blockly.DropDownDiv.getContentDiv().appendChild(picker);
  Blockly.DropDownDiv.setColour(
      this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR);

  Blockly.DropDownDiv.showPositionedByField(this);

  // Configure event handler on the table to listen for any event in a cell.
  Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker,
      'mouseup', this, this.onClick_);
};

/**
 * Handle a click on a colour cell.
 * @param {!Event} e Mouse event.
 * @private
 */
Blockly.FieldColour.prototype.onClick_ = function(e) {
  var cell = e.target;
  if (cell && !cell.label) {
    // The target element is the 'div', back out to the 'td'.
    cell = cell.parentNode;
  }
  var colour = cell && cell.label;
  Blockly.WidgetDiv.hide();
  if (this.sourceBlock_) {
    // Call any validation function, and allow it to override.
    colour = this.callValidator(colour);
  }
  if (colour !== null) {
    this.setValue(colour);
  }
};

/**
 * Create a colour picker widget.
 * @return {!Element} The newly created colour picker.
 * @private
 */
Blockly.FieldColour.prototype.createWidget_ = function() {
  var columns = this.columns_ || Blockly.FieldColour.COLUMNS;
  var colours = this.colours_ || Blockly.FieldColour.COLOURS;
  var titles = this.titles_ || Blockly.FieldColour.TITLES;
  var selectedColour = this.getValue();
  // Create the palette.
  var table = document.createElement('table');
  table.className = 'blocklyColourTable';
  var row;
  for (var i = 0; i < colours.length; i++) {
    if (i % columns == 0) {
      row = document.createElement('tr');
      table.appendChild(row);
    }
    var cell = document.createElement('td');
    row.appendChild(cell);
    var div = document.createElement('div');
    cell.appendChild(div);
    cell.label = colours[i];  // This becomes the value, if clicked.
    cell.title = titles[i] || colours[i];
    div.style.backgroundColor = colours[i];
    if (colours[i] == selectedColour) {
      div.className = 'blocklyColourSelected';
    }
  }
  return table;
};

/**
 * Hide the colour picker widget.
 * @private
 */
Blockly.FieldColour.widgetDispose_ = function() {
  if (Blockly.FieldColour.onUpWrapper_) {
    Blockly.unbindEvent_(Blockly.FieldColour.onUpWrapper_);
  }
  Blockly.Events.setGroup(false);
};

Blockly.Field.register('field_colour', Blockly.FieldColour);
<|endoftext|>


<commit_before>import GetSetBaseStore from './GetSetBaseStore';
import AppDispatcher from '../events/AppDispatcher';
import CosmosPackagesActions from '../events/CosmosPackagesActions';
import {
  COSMOS_SEARCH_CHANGE,
  COSMOS_SEARCH_ERROR,
  COSMOS_LIST_CHANGE,
  COSMOS_LIST_ERROR,
  COSMOS_DESCRIBE_CHANGE,
  COSMOS_DESCRIBE_ERROR,
  COSMOS_INSTALL_SUCCESS,
  COSMOS_INSTALL_ERROR,
  COSMOS_UNINSTALL_SUCCESS,
  COSMOS_UNINSTALL_ERROR,

  COSMOS_REPOSITORIES_SUCCESS,
  COSMOS_REPOSITORIES_ERROR,
  COSMOS_REPOSITORY_ADD_SUCCESS,
  COSMOS_REPOSITORY_ADD_ERROR,
  COSMOS_REPOSITORY_DELETE_SUCCESS,
  COSMOS_REPOSITORY_DELETE_ERROR
} from '../constants/EventTypes';
import {
  REQUEST_COSMOS_PACKAGES_LIST_ERROR,
  REQUEST_COSMOS_PACKAGES_LIST_SUCCESS,
  REQUEST_COSMOS_PACKAGES_SEARCH_ERROR,
  REQUEST_COSMOS_PACKAGES_SEARCH_SUCCESS,
  REQUEST_COSMOS_PACKAGE_DESCRIBE_ERROR,
  REQUEST_COSMOS_PACKAGE_DESCRIBE_SUCCESS,
  REQUEST_COSMOS_PACKAGE_INSTALL_ERROR,
  REQUEST_COSMOS_PACKAGE_INSTALL_SUCCESS,
  REQUEST_COSMOS_PACKAGE_UNINSTALL_ERROR,
  REQUEST_COSMOS_PACKAGE_UNINSTALL_SUCCESS,

  REQUEST_COSMOS_REPOSITORIES_LIST_SUCCESS,
  REQUEST_COSMOS_REPOSITORIES_LIST_ERROR,
  REQUEST_COSMOS_REPOSITORY_ADD_SUCCESS,
  REQUEST_COSMOS_REPOSITORY_ADD_ERROR,
  REQUEST_COSMOS_REPOSITORY_DELETE_SUCCESS,
  REQUEST_COSMOS_REPOSITORY_DELETE_ERROR,

  SERVER_ACTION
} from '../constants/ActionTypes';
import PluginSDK from 'PluginSDK';
import RepositoryList from '../structs/RepositoryList';
import UniverseInstalledPackagesList from
  '../structs/UniverseInstalledPackagesList';
import UniversePackage from '../structs/UniversePackage';
import UniversePackagesList from '../structs/UniversePackagesList';

class CosmosPackagesStore extends GetSetBaseStore {
  constructor() {
    super(...arguments);

    this.getSet_data = {
      availablePackages: [],
      packageDetails: null,
      installedPackages: [],
      repositories: []
    };

    PluginSDK.addStoreConfig({
      store: this,
      storeID: this.storeID,
      events: {
        availableError: COSMOS_SEARCH_ERROR,
        availableSuccess: COSMOS_SEARCH_CHANGE,
        descriptionSuccess: COSMOS_DESCRIBE_CHANGE,
        descriptionError: COSMOS_DESCRIBE_ERROR,
        installedSuccess: COSMOS_LIST_CHANGE,
        installedError: COSMOS_LIST_ERROR,

        installError: COSMOS_INSTALL_ERROR,
        installSuccess: COSMOS_INSTALL_SUCCESS,
        uninstallError: COSMOS_UNINSTALL_ERROR,
        uninstallSuccess: COSMOS_UNINSTALL_SUCCESS,

        repositoriesSuccess: COSMOS_REPOSITORIES_SUCCESS,
        repositoriesError: COSMOS_REPOSITORIES_ERROR,
        repositoryAddSuccess: COSMOS_REPOSITORY_ADD_SUCCESS,
        repositoryAddError: COSMOS_REPOSITORY_ADD_ERROR,
        repositoryDeleteSuccess: COSMOS_REPOSITORY_DELETE_SUCCESS,
        repositoryDeleteError: COSMOS_REPOSITORY_DELETE_ERROR
      },
      unmountWhen: function (store, event) {
        return event === 'availableSuccess';
      },
      listenAlways: false
    });

    this.dispatcherIndex = AppDispatcher.register((payload) => {
      let source = payload.source;
      if (source !== SERVER_ACTION) {
        return false;
      }

      let action = payload.action;
      let data = action.data;

      switch (action.type) {
        case REQUEST_COSMOS_PACKAGE_DESCRIBE_SUCCESS:
          this.processPackageDescriptionSuccess(
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_DESCRIBE_ERROR:
          this.processPackageDescriptionError(
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGES_LIST_SUCCESS:
          this.processInstalledPackagesSuccess(
            data,
            action.packageName,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGES_LIST_ERROR:
          this.processInstalledPackagesError(
            data,
            action.packageName,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGES_SEARCH_SUCCESS:
          this.processAvailablePackagesSuccess(
            data,
            action.query
          );
          break;
        case REQUEST_COSMOS_PACKAGES_SEARCH_ERROR:
          this.processAvailablePackagesError(
            data,
            action.query
          );
          break;
        case REQUEST_COSMOS_PACKAGE_INSTALL_SUCCESS:
          this.emit(
            COSMOS_INSTALL_SUCCESS,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_INSTALL_ERROR:
          this.emit(
            COSMOS_INSTALL_ERROR,
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_UNINSTALL_SUCCESS:
          this.emit(
            COSMOS_UNINSTALL_SUCCESS,
            action.packageName,
            action.packageVersion,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGE_UNINSTALL_ERROR:
          this.emit(
            COSMOS_UNINSTALL_ERROR,
            data,
            action.packageName,
            action.packageVersion,
            action.appId
          );
          break;
        case REQUEST_COSMOS_REPOSITORIES_LIST_SUCCESS:
          this.processRepositoriesSuccess(data);
          break;
        case REQUEST_COSMOS_REPOSITORIES_LIST_ERROR:
          this.emit(COSMOS_REPOSITORIES_ERROR, data);
          break;
        case REQUEST_COSMOS_REPOSITORY_ADD_SUCCESS:
          this.emit(
            COSMOS_REPOSITORY_ADD_SUCCESS, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_ADD_ERROR:
          this.emit(
            COSMOS_REPOSITORY_ADD_ERROR, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_DELETE_SUCCESS:
          this.emit(
            COSMOS_REPOSITORY_DELETE_SUCCESS, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_DELETE_ERROR:
          this.emit(
            COSMOS_REPOSITORY_DELETE_ERROR, data, action.name, action.uri
          );
          break;
      }

      return true;
    });
  }

  addChangeListener(eventName, callback) {
    this.on(eventName, callback);
  }

  removeChangeListener(eventName, callback) {
    this.removeListener(eventName, callback);
  }

  /* API */
  fetchAvailablePackages() {
    return CosmosPackagesActions.fetchAvailablePackages(...arguments);
  }

  fetchInstalledPackages() {
    return CosmosPackagesActions.fetchInstalledPackages(...arguments);
  }

  fetchPackageDescription() {
    return CosmosPackagesActions.fetchPackageDescription(...arguments);
  }

  installPackage() {
    return CosmosPackagesActions.installPackage(...arguments);
  }

  uninstallPackage() {
    return CosmosPackagesActions.uninstallPackage(...arguments);
  }

  fetchRepositories() {
    return CosmosPackagesActions.fetchRepositories(...arguments);
  }

  addRepository() {
    return CosmosPackagesActions.addRepository(...arguments);
  }

  deleteRepository() {
    return CosmosPackagesActions.deleteRepository(...arguments);
  }

  /* Reducers */
  getAvailablePackages() {
    return new UniversePackagesList({items: this.get('availablePackages')});
  }

  getInstalledPackages() {
    return new UniverseInstalledPackagesList(
      {items: this.get('installedPackages')}
    );
  }

  getPackageDetails() {
    let packageDetails = this.get('packageDetails');
    if (packageDetails) {
      return new UniversePackage(packageDetails);
    }

    return null;
  }

  getRepositories() {
    return new RepositoryList({items: this.get('repositories')});
  }

  processAvailablePackagesSuccess(packages, query) {
    this.set({availablePackages: packages});

    this.emit(COSMOS_SEARCH_CHANGE, query);
  }

  processAvailablePackagesError(error, query) {
    this.emit(COSMOS_SEARCH_ERROR, error, query);
  }

  processInstalledPackagesSuccess(packages, name, appId) {
    this.set({installedPackages: packages});

    this.emit(COSMOS_LIST_CHANGE, name, appId);
  }

  processInstalledPackagesError(error, name, appId) {
    this.emit(COSMOS_LIST_ERROR, error, name, appId);
  }

  processPackageDescriptionSuccess(cosmosPackage, name, version) {
    this.set({packageDetails: cosmosPackage});

    this.emit(COSMOS_DESCRIBE_CHANGE, name, version);
  }

  processPackageDescriptionError(error, name, version) {
    this.set({packageDetails: null});

    this.emit(COSMOS_DESCRIBE_ERROR, error, name, version);
  }

  processRepositoriesSuccess(repositories) {
    this.set({repositories});

    this.emit(COSMOS_REPOSITORIES_SUCCESS);
  }

  get storeID() {
    return 'cosmosPackages';
  }
}

module.exports = new CosmosPackagesStore();
<commit_msg>:abc:<commit_after>import GetSetBaseStore from './GetSetBaseStore';
import AppDispatcher from '../events/AppDispatcher';
import CosmosPackagesActions from '../events/CosmosPackagesActions';
import {
  COSMOS_DESCRIBE_CHANGE,
  COSMOS_DESCRIBE_ERROR,
  COSMOS_INSTALL_ERROR,
  COSMOS_INSTALL_SUCCESS,
  COSMOS_LIST_CHANGE,
  COSMOS_LIST_ERROR,
  COSMOS_SEARCH_CHANGE,
  COSMOS_SEARCH_ERROR,
  COSMOS_UNINSTALL_ERROR,
  COSMOS_UNINSTALL_SUCCESS,

  COSMOS_REPOSITORIES_ERROR,
  COSMOS_REPOSITORIES_SUCCESS,
  COSMOS_REPOSITORY_ADD_ERROR,
  COSMOS_REPOSITORY_ADD_SUCCESS,
  COSMOS_REPOSITORY_DELETE_ERROR,
  COSMOS_REPOSITORY_DELETE_SUCCESS
} from '../constants/EventTypes';
import {
  REQUEST_COSMOS_PACKAGE_DESCRIBE_ERROR,
  REQUEST_COSMOS_PACKAGE_DESCRIBE_SUCCESS,
  REQUEST_COSMOS_PACKAGE_INSTALL_ERROR,
  REQUEST_COSMOS_PACKAGE_INSTALL_SUCCESS,
  REQUEST_COSMOS_PACKAGE_UNINSTALL_ERROR,
  REQUEST_COSMOS_PACKAGE_UNINSTALL_SUCCESS,
  REQUEST_COSMOS_PACKAGES_LIST_ERROR,
  REQUEST_COSMOS_PACKAGES_LIST_SUCCESS,
  REQUEST_COSMOS_PACKAGES_SEARCH_ERROR,
  REQUEST_COSMOS_PACKAGES_SEARCH_SUCCESS,

  REQUEST_COSMOS_REPOSITORIES_LIST_ERROR,
  REQUEST_COSMOS_REPOSITORIES_LIST_SUCCESS,
  REQUEST_COSMOS_REPOSITORY_ADD_ERROR,
  REQUEST_COSMOS_REPOSITORY_ADD_SUCCESS,
  REQUEST_COSMOS_REPOSITORY_DELETE_ERROR,
  REQUEST_COSMOS_REPOSITORY_DELETE_SUCCESS,

  SERVER_ACTION
} from '../constants/ActionTypes';
import PluginSDK from 'PluginSDK';
import RepositoryList from '../structs/RepositoryList';
import UniverseInstalledPackagesList from
  '../structs/UniverseInstalledPackagesList';
import UniversePackage from '../structs/UniversePackage';
import UniversePackagesList from '../structs/UniversePackagesList';

class CosmosPackagesStore extends GetSetBaseStore {
  constructor() {
    super(...arguments);

    this.getSet_data = {
      availablePackages: [],
      packageDetails: null,
      installedPackages: [],
      repositories: []
    };

    PluginSDK.addStoreConfig({
      store: this,
      storeID: this.storeID,
      events: {
        availableError: COSMOS_SEARCH_ERROR,
        availableSuccess: COSMOS_SEARCH_CHANGE,
        descriptionSuccess: COSMOS_DESCRIBE_CHANGE,
        descriptionError: COSMOS_DESCRIBE_ERROR,
        installedSuccess: COSMOS_LIST_CHANGE,
        installedError: COSMOS_LIST_ERROR,

        installError: COSMOS_INSTALL_ERROR,
        installSuccess: COSMOS_INSTALL_SUCCESS,
        uninstallError: COSMOS_UNINSTALL_ERROR,
        uninstallSuccess: COSMOS_UNINSTALL_SUCCESS,

        repositoriesSuccess: COSMOS_REPOSITORIES_SUCCESS,
        repositoriesError: COSMOS_REPOSITORIES_ERROR,
        repositoryAddSuccess: COSMOS_REPOSITORY_ADD_SUCCESS,
        repositoryAddError: COSMOS_REPOSITORY_ADD_ERROR,
        repositoryDeleteSuccess: COSMOS_REPOSITORY_DELETE_SUCCESS,
        repositoryDeleteError: COSMOS_REPOSITORY_DELETE_ERROR
      },
      unmountWhen: function (store, event) {
        return event === 'availableSuccess';
      },
      listenAlways: false
    });

    this.dispatcherIndex = AppDispatcher.register((payload) => {
      let source = payload.source;
      if (source !== SERVER_ACTION) {
        return false;
      }

      let action = payload.action;
      let data = action.data;

      switch (action.type) {
        case REQUEST_COSMOS_PACKAGE_DESCRIBE_SUCCESS:
          this.processPackageDescriptionSuccess(
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_DESCRIBE_ERROR:
          this.processPackageDescriptionError(
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGES_LIST_SUCCESS:
          this.processInstalledPackagesSuccess(
            data,
            action.packageName,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGES_LIST_ERROR:
          this.processInstalledPackagesError(
            data,
            action.packageName,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGES_SEARCH_SUCCESS:
          this.processAvailablePackagesSuccess(
            data,
            action.query
          );
          break;
        case REQUEST_COSMOS_PACKAGES_SEARCH_ERROR:
          this.processAvailablePackagesError(
            data,
            action.query
          );
          break;
        case REQUEST_COSMOS_PACKAGE_INSTALL_SUCCESS:
          this.emit(
            COSMOS_INSTALL_SUCCESS,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_INSTALL_ERROR:
          this.emit(
            COSMOS_INSTALL_ERROR,
            data,
            action.packageName,
            action.packageVersion
          );
          break;
        case REQUEST_COSMOS_PACKAGE_UNINSTALL_SUCCESS:
          this.emit(
            COSMOS_UNINSTALL_SUCCESS,
            action.packageName,
            action.packageVersion,
            action.appId
          );
          break;
        case REQUEST_COSMOS_PACKAGE_UNINSTALL_ERROR:
          this.emit(
            COSMOS_UNINSTALL_ERROR,
            data,
            action.packageName,
            action.packageVersion,
            action.appId
          );
          break;
        case REQUEST_COSMOS_REPOSITORIES_LIST_SUCCESS:
          this.processRepositoriesSuccess(data);
          break;
        case REQUEST_COSMOS_REPOSITORIES_LIST_ERROR:
          this.emit(COSMOS_REPOSITORIES_ERROR, data);
          break;
        case REQUEST_COSMOS_REPOSITORY_ADD_SUCCESS:
          this.emit(
            COSMOS_REPOSITORY_ADD_SUCCESS, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_ADD_ERROR:
          this.emit(
            COSMOS_REPOSITORY_ADD_ERROR, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_DELETE_SUCCESS:
          this.emit(
            COSMOS_REPOSITORY_DELETE_SUCCESS, data, action.name, action.uri
          );
          break;
        case REQUEST_COSMOS_REPOSITORY_DELETE_ERROR:
          this.emit(
            COSMOS_REPOSITORY_DELETE_ERROR, data, action.name, action.uri
          );
          break;
      }

      return true;
    });
  }

  addChangeListener(eventName, callback) {
    this.on(eventName, callback);
  }

  removeChangeListener(eventName, callback) {
    this.removeListener(eventName, callback);
  }

  /* API */
  fetchAvailablePackages() {
    return CosmosPackagesActions.fetchAvailablePackages(...arguments);
  }

  fetchInstalledPackages() {
    return CosmosPackagesActions.fetchInstalledPackages(...arguments);
  }

  fetchPackageDescription() {
    return CosmosPackagesActions.fetchPackageDescription(...arguments);
  }

  installPackage() {
    return CosmosPackagesActions.installPackage(...arguments);
  }

  uninstallPackage() {
    return CosmosPackagesActions.uninstallPackage(...arguments);
  }

  fetchRepositories() {
    return CosmosPackagesActions.fetchRepositories(...arguments);
  }

  addRepository() {
    return CosmosPackagesActions.addRepository(...arguments);
  }

  deleteRepository() {
    return CosmosPackagesActions.deleteRepository(...arguments);
  }

  /* Reducers */
  getAvailablePackages() {
    return new UniversePackagesList({items: this.get('availablePackages')});
  }

  getInstalledPackages() {
    return new UniverseInstalledPackagesList(
      {items: this.get('installedPackages')}
    );
  }

  getPackageDetails() {
    let packageDetails = this.get('packageDetails');
    if (packageDetails) {
      return new UniversePackage(packageDetails);
    }

    return null;
  }

  getRepositories() {
    return new RepositoryList({items: this.get('repositories')});
  }

  processAvailablePackagesSuccess(packages, query) {
    this.set({availablePackages: packages});

    this.emit(COSMOS_SEARCH_CHANGE, query);
  }

  processAvailablePackagesError(error, query) {
    this.emit(COSMOS_SEARCH_ERROR, error, query);
  }

  processInstalledPackagesSuccess(packages, name, appId) {
    this.set({installedPackages: packages});

    this.emit(COSMOS_LIST_CHANGE, name, appId);
  }

  processInstalledPackagesError(error, name, appId) {
    this.emit(COSMOS_LIST_ERROR, error, name, appId);
  }

  processPackageDescriptionSuccess(cosmosPackage, name, version) {
    this.set({packageDetails: cosmosPackage});

    this.emit(COSMOS_DESCRIBE_CHANGE, name, version);
  }

  processPackageDescriptionError(error, name, version) {
    this.set({packageDetails: null});

    this.emit(COSMOS_DESCRIBE_ERROR, error, name, version);
  }

  processRepositoriesSuccess(repositories) {
    this.set({repositories});

    this.emit(COSMOS_REPOSITORIES_SUCCESS);
  }

  get storeID() {
    return 'cosmosPackages';
  }
}

module.exports = new CosmosPackagesStore();
<|endoftext|>


<commit_before>import React, { Fragment, Component } from 'react'
import styled, { css } from 'styled-components'
import {
  Box,
  Flex,
  Container,
  Heading,
  Text,
  Section,
  Link,
  Card,
  Button,
  Icon,
  Image
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Nav from 'components/Nav'
import Footer from 'components/Footer'
import IconButton from 'components/IconButton'
import Form from 'components/challenge/Form'
import Ended from 'components/challenge/Ended'
import Posts from 'components/challenge/Posts'
import DiscussChallenge from 'components/challenge/DiscussChallenge'
import { Modal, Overlay, CloseButton } from 'components/Modal'
import {
  DropdownContainer,
  DropdownMenu,
  DropdownMenuOption
} from 'components/Dropdown'
import { dt, tinyDt } from 'helpers'
import { isEmpty, keys } from 'lodash'
import api from 'api'
import storage from 'storage'

const sortByHumanized = {
  trending: 'Trending',
  top: 'Top-voted',
  newest: 'Newest',
  viewed: 'Top-viewed',
  random: 'Random'
}

class Help extends Component {
  state = { active: false }

  toggleRules = () => {
    this.setState(state => ({ active: !state.active }))
  }

  // Render a modal for challenge rules on button press
  render() {
    if (this.state.active) {
      return (
        <Fragment>
          <Modal align="left" my={4} p={[3, 4]}>
            <CloseButton onClick={this.toggleRules} />
            <Heading.h2>Challenge Rules</Heading.h2>
            {/*
            <Text
              f={2}
              mt={3}
              color="info"
              py={2}
              px={3}
              bg="blue.0"
              style={{ borderRadius: 4 }}
            >
              For this challenge, your entry must use{' '}
              <Link href="https://p5js.org" target="_blank" bold>
                p5.js
              </Link>{' '}
              in its code.
            </Text>
            */}
            <Text f={2} my={3}>
              Challenge is open to Hack Club members and repl.it users. It
              strictly follows Hack Club’s{' '}
              <Link href="https://conduct.hackclub.com" target="_blank">
                Code of Conduct
              </Link>. Anything breaking our Code of Conduct (ex. voter fraud)
              and contestants found cheating can be temporarily or permanently
              banned.
            </Text>
            <Text f={2}>
              If you think anyone has violated our Conduct or cheating policy,
              please reach out to us confidentially at{' '}
              <Link href="mailto:challenge@hackclub.com">
                challenge@hackclub.com
              </Link>.
            </Text>
          </Modal>
          <Overlay onClick={this.toggleRules} />
        </Fragment>
      )
    }
    return (
      <IconButton
        name="flag"
        size={16}
        children="Rules"
        inverted
        f={2}
        onClick={this.toggleRules}
        style={{ float: 'right' }}
        {...this.props}
      />
    )
  }
}

const Header = Section.withComponent('header').extend`
  background-color: ${({ theme }) => theme.colors.red[5]};
  background-image: linear-gradient(
    32deg,
    ${({ theme }) => theme.colors.pink[5]},
    ${({ theme }) => theme.colors.red[5]}
  );
`

const HeaderContainer = Container.extend`
  display: grid;
  grid-gap: ${({ theme }) => theme.space[3]}px;
  grid-template-areas: 'text' 'info' 'form';
  ${({ theme }) => theme.mediaQueries.md} {
    grid-template-columns: repeat(2, 1fr);
    grid-gap: ${({ theme }) => theme.space[4]}px;
      ${props =>
        props.success
          ? css`
              grid-template-areas:
                'text form'
                'info form';
              ${HeaderAreaText} {
                text-align: right;
              }
            `
          : css`
              grid-template-areas:
                'text text'
                'info form';
            `};
    }
  }
`

const HeaderCard = styled(Card)`
  h2,
  p {
    color: ${({ theme }) => theme.colors.black} !important;
  }
`
HeaderCard.defaultProps = {
  boxShadowSize: 'md',
  p: 3,
  bg: 'pink.0',
  align: 'left'
}

const HeaderAreaText = styled(Box)`
  grid-area: text;
`
const HeaderAreaInfo = styled(HeaderCard)`
  grid-area: info;
`
const HeaderAreaForm = styled(HeaderCard)`
  grid-area: form;
`

const Title = styled(Flex)`
  border-bottom: 1px solid ${({ theme }) => theme.colors.smoke};
`

const title = 'Hack Club Challenge'
const desc =
  'Join Hack Club’s high school coding challenge. Submit your entry to compete in our monthly programming contest and win prizes.'
const img = 'https://hackclub.com/challenge.png'

export default class extends Component {
  state = { status: 'loading', sortBy: 'top' }

  componentDidMount() {
    if (storage.get('authToken')) {
      api
        .get(`v1/users/current`)
        .then(user => {
          this.setState({ status: 'success', userId: user.id })
        })
        .catch(err => {
          if (err.status === 401) {
            this.setState({ status: 'needsToAuth' })
          } else {
            this.setState({ status: 'error' })
          }
        })
    } else {
      this.setState({ status: 'needsToAuth' })
    }
  }

  render() {
    const { data } = this.props
    const { userId, status, sortBy } = this.state
    if (isEmpty(data)) return null
    const challenge = data.allChallengesJson.edges[0].node
    const ended = Date.parse(new Date()) > Date.parse(challenge.end)
    return (
      <Fragment>
        <Helmet
          title={title}
          meta={[
            { name: 'description', content: desc },
            { name: 'twitter:title', content: title },
            { name: 'twitter:description', content: desc },
            { name: 'twitter:image', content: img },
            { property: 'og:title', content: title },
            { property: 'og:description', content: desc },
            { property: 'og:image', content: img },
            { property: 'og:url', content: 'https://hackclub.com/challenge' }
          ]}
        />

          <HeaderContainer
            pt={[4, 5]}
            pb={[3, 4]}
            align="left"
            success={status === 'success'}
          >
            <HeaderAreaText align="center" mt={3}>
              <Text color="pink.0" mb={[-2, -3]} f={3} bold caps>
                Hack Club
              </Text>
              <Heading.h1 f={[6, 7]} my={0}>
                Challenge
              </Heading.h1>
            </HeaderAreaText>
            <HeaderAreaInfo>
              <Text f={2}>
                🌟 Challenge: {challenge.name}

                🎁 {challenge.description}

                ℹ Submissions open to Hack Club community members

                📖{' '}
                <Link
                  href="/workshops/challenge_ridiculous_api"
                  target="_blank"
                  underline
                >
                  Click here
                </Link>{' '}
                for help getting started

                🏅 Submissions due {dt(challenge.end)}. Top 3 voted win!
              </Text>
            </HeaderAreaInfo>
            <HeaderAreaForm>
              <Help />
              <Form challengeId={challenge.id} status={status} />
            </HeaderAreaForm>
          </HeaderContainer>

        <Container maxWidth={48} pt={4} pb={5} px={[0, 3]}>
          {ended && <Ended />}
          <Title align="center" pb={2} px={[2, 0]}>
            <Flex align="center" flex="1 1 auto" wrap>
              <Heading.h2 color="black" f={5} mr={2}>
                Submissions
              </Heading.h2>
              <Text f={2} color="muted" align="left">
                {tinyDt(challenge.start)}–{tinyDt(challenge.end)}
              </Text>
            </Flex>
            <DropdownContainer>
              <IconButton
                name="sort"
                size={16}
                bg="info"
                f={2}
                style={{ whiteSpace: 'nowrap' }}
                children={sortByHumanized[sortBy]}
              />
              <DropdownMenu>
                {keys(sortByHumanized).map(key => (
                  <DropdownMenuOption
                    active={sortBy === key}
                    onClick={() => this.setState({ sortBy: key })}
                    children={sortByHumanized[key]}
                    key={key}
                  />
                ))}
              </DropdownMenu>
            </DropdownContainer>
          </Title>
          <Posts
            challengeId={challenge.id}
            userId={userId}
            status={status}
            sortBy={sortBy}
          />
          <DiscussChallenge />
        </Container>

      </Fragment>
    )
  }
}
export const pageQuery = graphql`
  query ChallengeQuery {
    allChallengesJson(
      filter: { name: { ne: "T-Shirt" } }
      sort: { fields: [start], order: DESC }
      limit: 1
    ) {
      edges {
        node {
          id
          name
          description
          start
          end
        }
      }
    }
  }
`
<commit_msg>Lock to Challenge #4<commit_after>import React, { Fragment, Component } from 'react'
import styled, { css } from 'styled-components'
import {
  Box,
  Flex,
  Container,
  Heading,
  Text,
  Section,
  Link,
  Card,
  Button,
  Icon,
  Image
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Nav from 'components/Nav'
import Footer from 'components/Footer'
import IconButton from 'components/IconButton'
import Form from 'components/challenge/Form'
import Ended from 'components/challenge/Ended'
import Posts from 'components/challenge/Posts'
import DiscussChallenge from 'components/challenge/DiscussChallenge'
import { Modal, Overlay, CloseButton } from 'components/Modal'
import {
  DropdownContainer,
  DropdownMenu,
  DropdownMenuOption
} from 'components/Dropdown'
import { dt, tinyDt } from 'helpers'
import { isEmpty, keys } from 'lodash'
import api from 'api'
import storage from 'storage'

const sortByHumanized = {
  trending: 'Trending',
  top: 'Top-voted',
  newest: 'Newest',
  viewed: 'Top-viewed',
  random: 'Random'
}

class Help extends Component {
  state = { active: false }

  toggleRules = () => {
    this.setState(state => ({ active: !state.active }))
  }

  // Render a modal for challenge rules on button press
  render() {
    if (this.state.active) {
      return (
        <Fragment>
          <Modal align="left" my={4} p={[3, 4]}>
            <CloseButton onClick={this.toggleRules} />
            <Heading.h2>Challenge Rules</Heading.h2>
            {/*
            <Text
              f={2}
              mt={3}
              color="info"
              py={2}
              px={3}
              bg="blue.0"
              style={{ borderRadius: 4 }}
            >
              For this challenge, your entry must use{' '}
              <Link href="https://p5js.org" target="_blank" bold>
                p5.js
              </Link>{' '}
              in its code.
            </Text>
            */}
            <Text f={2} my={3}>
              Challenge is open to Hack Club members and repl.it users. It
              strictly follows Hack Club’s{' '}
              <Link href="https://conduct.hackclub.com" target="_blank">
                Code of Conduct
              </Link>. Anything breaking our Code of Conduct (ex. voter fraud)
              and contestants found cheating can be temporarily or permanently
              banned.
            </Text>
            <Text f={2}>
              If you think anyone has violated our Conduct or cheating policy,
              please reach out to us confidentially at{' '}
              <Link href="mailto:challenge@hackclub.com">
                challenge@hackclub.com
              </Link>.
            </Text>
          </Modal>
          <Overlay onClick={this.toggleRules} />
        </Fragment>
      )
    }
    return (
      <IconButton
        name="flag"
        size={16}
        children="Rules"
        inverted
        f={2}
        onClick={this.toggleRules}
        style={{ float: 'right' }}
        {...this.props}
      />
    )
  }
}

const Header = Section.withComponent('header').extend`
  background-color: ${({ theme }) => theme.colors.red[5]};
  background-image: linear-gradient(
    32deg,
    ${({ theme }) => theme.colors.pink[5]},
    ${({ theme }) => theme.colors.red[5]}
  );
`

const HeaderContainer = Container.extend`
  display: grid;
  grid-gap: ${({ theme }) => theme.space[3]}px;
  grid-template-areas: 'text' 'info' 'form';
  ${({ theme }) => theme.mediaQueries.md} {
    grid-template-columns: repeat(2, 1fr);
    grid-gap: ${({ theme }) => theme.space[4]}px;
      ${props =>
        props.success
          ? css`
              grid-template-areas:
                'text form'
                'info form';
              ${HeaderAreaText} {
                text-align: right;
              }
            `
          : css`
              grid-template-areas:
                'text text'
                'info form';
            `};
    }
  }
`

const HeaderCard = styled(Card)`
  h2,
  p {
    color: ${({ theme }) => theme.colors.black} !important;
  }
`
HeaderCard.defaultProps = {
  boxShadowSize: 'md',
  p: 3,
  bg: 'pink.0',
  align: 'left'
}

const HeaderAreaText = styled(Box)`
  grid-area: text;
`
const HeaderAreaInfo = styled(HeaderCard)`
  grid-area: info;
`
const HeaderAreaForm = styled(HeaderCard)`
  grid-area: form;
`

const Title = styled(Flex)`
  border-bottom: 1px solid ${({ theme }) => theme.colors.smoke};
`

const title = 'Hack Club Challenge'
const desc =
  'Join Hack Club’s high school coding challenge. Submit your entry to compete in our monthly programming contest and win prizes.'
const img = 'https://hackclub.com/challenge.png'

export default class extends Component {
  state = { status: 'loading', sortBy: 'top' }

  componentDidMount() {
    if (storage.get('authToken')) {
      api
        .get(`v1/users/current`)
        .then(user => {
          this.setState({ status: 'success', userId: user.id })
        })
        .catch(err => {
          if (err.status === 401) {
            this.setState({ status: 'needsToAuth' })
          } else {
            this.setState({ status: 'error' })
          }
        })
    } else {
      this.setState({ status: 'needsToAuth' })
    }
  }

  render() {
    const { data } = this.props
    const { userId, status, sortBy } = this.state
    if (isEmpty(data)) return null
    const challenge = data.allChallengesJson.edges[0].node
    const ended = Date.parse(new Date()) > Date.parse(challenge.end)
    return (
      <Fragment>
        <Helmet
          title={title}
          meta={[
            { name: 'description', content: desc },
            { name: 'twitter:title', content: title },
            { name: 'twitter:description', content: desc },
            { name: 'twitter:image', content: img },
            { property: 'og:title', content: title },
            { property: 'og:description', content: desc },
            { property: 'og:image', content: img },
            { property: 'og:url', content: 'https://hackclub.com/challenge' }
          ]}
        />

          <HeaderContainer
            pt={[4, 5]}
            pb={[3, 4]}
            align="left"
            success={status === 'success'}
          >
            <HeaderAreaText align="center" mt={3}>
              <Text color="pink.0" mb={[-2, -3]} f={3} bold caps>
                Hack Club
              </Text>
              <Heading.h1 f={[6, 7]} my={0}>
                Challenge
              </Heading.h1>
            </HeaderAreaText>
            <HeaderAreaInfo>
              <Text f={2}>
                🌟 Challenge: {challenge.name}

                🎁 {challenge.description}

                ℹ Submissions open to Hack Club community members

                📖{' '}
                <Link
                  href="/workshops/challenge_ridiculous_api"
                  target="_blank"
                  underline
                >
                  Click here
                </Link>{' '}
                for help getting started

                🏅 Submissions due {dt(challenge.end)}. Top 3 voted win!
              </Text>
            </HeaderAreaInfo>
            <HeaderAreaForm>
              <Help />
              <Form challengeId={challenge.id} status={status} />
            </HeaderAreaForm>
          </HeaderContainer>

        <Container maxWidth={48} pt={4} pb={5} px={[0, 3]}>
          {ended && <Ended />}
          <Title align="center" pb={2} px={[2, 0]}>
            <Flex align="center" flex="1 1 auto" wrap>
              <Heading.h2 color="black" f={5} mr={2}>
                Submissions
              </Heading.h2>
              <Text f={2} color="muted" align="left">
                {tinyDt(challenge.start)}–{tinyDt(challenge.end)}
              </Text>
            </Flex>
            <DropdownContainer>
              <IconButton
                name="sort"
                size={16}
                bg="info"
                f={2}
                style={{ whiteSpace: 'nowrap' }}
                children={sortByHumanized[sortBy]}
              />
              <DropdownMenu>
                {keys(sortByHumanized).map(key => (
                  <DropdownMenuOption
                    active={sortBy === key}
                    onClick={() => this.setState({ sortBy: key })}
                    children={sortByHumanized[key]}
                    key={key}
                  />
                ))}
              </DropdownMenu>
            </DropdownContainer>
          </Title>
          <Posts
            challengeId={challenge.id}
            userId={userId}
            status={status}
            sortBy={sortBy}
          />
          <DiscussChallenge />
        </Container>

      </Fragment>
    )
  }
}
export const pageQuery = graphql`
  query ChallengeQuery {
    allChallengesJson(
      filter: { id: { eq: "4" } }
      sort: { fields: [start], order: DESC }
      limit: 1
    ) {
      edges {
        node {
          id
          name
          description
          start
          end
        }
      }
    }
  }
`
<|endoftext|>