Initial work on simple ComparisonTable lib
13 files changed, 448 insertions(+), 0 deletions(-)

A => .hgignore
A => README.md
A => babelrc.config.js
A => config/default.json
A => config/production.json
A => package.json
A => postcss.config.js
A => src/index.js
A => src/lib/config.js
A => src/lib/dom.js
A => src/lib/error.js
A => src/styles.scss
A => webpack.config.js
A => .hgignore +4 -0
@@ 0,0 1,4 @@ 
+node_modules
+build
+.DS_Store
+.vscode
  No newline at end of file

          
A => README.md +0 -0

        
A => babelrc.config.js +14 -0
@@ 0,0 1,14 @@ 
+module.exports = {
+    presets: [
+        "@babel/preset-env",
+    ],
+    plugins: [
+        '@babel/plugin-transform-runtime',
+        ['@babel/plugin-proposal-class-properties', { 'loose': true }],
+        ['@babel/plugin-proposal-decorators', { 'legacy': true }],
+        '@babel/plugin-transform-async-to-generator',
+        '@babel/plugin-transform-arrow-functions',
+        '@babel/plugin-proposal-object-rest-spread',
+        '@babel/plugin-proposal-export-default-from'
+    ]
+}
  No newline at end of file

          
A => config/default.json +6 -0
@@ 0,0 1,6 @@ 
+{
+    "uglify": false,
+    "sourcemap": false,
+    "open": true,
+    "publicPath": "/"
+}
  No newline at end of file

          
A => config/production.json +6 -0
@@ 0,0 1,6 @@ 
+{
+    "uglify": true,
+    "sourcemap": true,
+    "open": false,
+    "publicPath": "./"
+}
  No newline at end of file

          
A => package.json +54 -0
@@ 0,0 1,54 @@ 
+{
+  "name": "jana",
+  "version": "1.0.0",
+  "description": "VanillaJS comparison table",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "NODE_ENV=production webpack",
+    "start": "webpack-dev-server"
+  },
+  "repository": {
+    "type": "mercurial",
+    "url": ""
+  },
+  "engines": {
+    "node": ">=14.15.3"
+  },
+  "keywords": [
+    "table",
+    "compare",
+    "vanilla"
+  ],
+  "author": "Oscar Cortez <oscar@netlandish.com>",
+  "license": "MIT",
+  "bugs": {
+    "url": "--"
+  },
+  "homepage": "",
+  "devDependencies": {
+    "@babel/core": "^7.12.10",
+    "@babel/plugin-proposal-class-properties": "^7.12.1",
+    "@babel/plugin-proposal-decorators": "^7.12.12",
+    "@babel/plugin-proposal-export-default-from": "^7.12.1",
+    "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
+    "@babel/plugin-transform-arrow-functions": "^7.12.1",
+    "@babel/plugin-transform-async-to-generator": "^7.12.1",
+    "@babel/plugin-transform-runtime": "^7.12.10",
+    "@babel/preset-env": "^7.12.11",
+    "@babel/preset-react": "^7.12.10",
+    "autoprefixer": "^9.8.6",
+    "babel-loader": "^8.2.2",
+    "config": "^3.3.3",
+    "css-loader": "^3.6.0",
+    "cssnano": "^4.1.10",
+    "html-webpack-plugin": "^3.2.0",
+    "node-sass": "^4.14.1",
+    "postcss-loader": "^3.0.0",
+    "sass-loader": "^8.0.2",
+    "style-loader": "^1.3.0",
+    "webpack": "^4.44.2",
+    "webpack-cli": "^3.3.12",
+    "webpack-dev-server": "^3.11.0"
+  },
+  "dependencies": {}
+}

          
A => postcss.config.js +6 -0
@@ 0,0 1,6 @@ 
+module.exports = {
+    plugins: [
+        require('autoprefixer'),
+        require('cssnano'),
+    ]
+};
  No newline at end of file

          
A => src/index.js +122 -0
@@ 0,0 1,122 @@ 
+import './styles.scss';
+import defaultConfig from './lib/config';
+import ValidationError from './lib/error';
+import { QuestionTemplate, AnswerTemplate } from './lib/dom';
+
+
+class ComparisonTable {
+    constructor(container, questions, answers, config) {
+        this.container = container;
+        this.questions = questions;
+        this.answers = answers;
+        this.config = Object.assign(defaultConfig, config);
+
+        this.mode = 'a'; // q -> Question | a -> Answer
+        this.questionIndex = 0;
+        this.answerIndex = 0;
+        this.selected = [];
+        this.initialized = false;
+    };
+
+    addStep(question, answer, index) {
+        console.log(question, answer)
+    };
+
+    addEvents() {
+        const _this = this;
+        document.addEventListener('keydown', (event) => {
+            if (event.key === 'n') {
+                if (_this.mode === 'q') {
+                    _this.selected.push(false);
+                };
+                _this.render();
+            };
+            if (event.key === 'y') {
+                _this.selected.push(true);
+                _this.render();
+            };
+        });
+        document.addEventListener('questionNext', (event) => {
+            _this.mode = 'q';
+            _this.render();
+        });
+        document.addEventListener('questionYes', (event) => {
+            _this.selected.push(false);
+            _this.render();
+        });
+        document.addEventListener('questionNo', (event) => {
+            _this.selected.push(false);
+            _this.render();
+        });
+    };
+
+    renderQuestion(question) {
+        const rendered_question = QuestionTemplate(this.questionIndex, this.questions.length, question['title']);
+        this.container.innerHTML = rendered_question;
+    };
+
+    renderAnswer(answer) {
+        if (this.config['options'].length !== Object.keys(answer).length) {
+            throw new ValidationError('Not enought options for the answer');
+        };
+        const question_title = this.questions[this.questionIndex - 1]['title'];
+        const formatted_answer = this.config['options'].map(item => {
+            let formatted = {};
+            Object.assign(formatted, item);
+            formatted['title'] = answer[item['key']];
+            return formatted;
+        });
+        const rendered_answer = AnswerTemplate(this.questionIndex - 1, this.questions.length, question_title, formatted_answer);
+        this.container.innerHTML = rendered_answer;
+    };
+
+    renderFinal() {
+        this.container.innerHTML = '<h1>The Final</h1>';
+    };
+
+    nextQuestion() {
+        const question = this.questions[this.questionIndex]
+        this.renderQuestion(question);
+        if (this.config.onNext && (typeof this.config.onNext == 'function')) {
+            this.config.onNext();
+        };
+        this.questionIndex += 1;
+    };
+
+    nextAnswer() {
+        const answer = this.answers[this.answerIndex];
+        this.renderAnswer(answer);
+        if (this.config.onNext && (typeof this.config.onNext == 'function')) {
+            this.config.onNext();
+        };
+        this.answerIndex += 1;
+    };
+
+    render() {
+        if (this.questionIndex === this.questions.length && this.answerIndex === this.answers.length) {
+            this.renderFinal();
+        } else if (this.mode === 'q') {
+            this.nextQuestion();
+            this.mode = 'a';
+        } else if (this.mode === 'a') {
+            this.nextAnswer();
+            this.mode = 'q';
+        };
+    };
+
+    init() {
+        if (this.questions.length !== this.answers.length) {
+            throw new ValidationError('Questions and Answer should be equals in size');
+        };
+        this.addEvents();
+        this.renderQuestion(this.questions[this.questionIndex]);
+        this.questionIndex = 1;
+        if (this.config.onInit && (typeof this.config.onInit == 'function')) {
+            this.config.onInit();
+        };
+        this.initialized = true;
+    };
+
+};
+
+export default ComparisonTable;

          
A => src/lib/config.js +35 -0
@@ 0,0 1,35 @@ 
+export default {
+    konami: {
+        enabled: true,
+        yes: 89,
+        no: 78,
+        next: 78,
+    },
+    yes: {
+        text: 'Yes',
+        key: 'y',
+        color: '',
+    },
+    no: {
+        text: 'No',
+        key: 'n',
+        color: '',
+    },
+    next: {
+        text: 'Next',
+        key: 'n',
+        color: '',
+    },
+    onInit: function () {
+        console.log('')
+    },
+    onSelected: function () {
+        console.log('')
+    },
+    onNext: function () {
+        console.log('')
+    },
+    onFinish: function () {
+        console.log('Finish')
+    },
+}
  No newline at end of file

          
A => src/lib/dom.js +36 -0
@@ 0,0 1,36 @@ 
+export const QuestionTemplate = (index, total, question) => `
+    <div class="question">
+        <svg viewBox="0 0 36 36" class="progress-circle">
+            <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" class="progress-circle__text">${index}/${total}</text>
+            <path d="M18 2 a 16 16 0 0 1 0 32 a 16 16 0 0 1 0 -32" class="progress-circle__background"></path>
+            <path d="M18 2 a 16 16 0 0 1 0 32 a 16 16 0 0 1 0 -32" class="progress-circle__completed" style="stroke-dasharray: ${Math.round(index*100/total)}px, 100px;"></path>
+        </svg>
+        <h2 class="question__title">${question}</h2>
+        <div>
+            <button class="question__action" onclick="document.dispatchEvent(new Event('questionYes'))">Yes</button>
+            <button class="question__action" onclick="document.dispatchEvent(new Event('questionNo'))">No</button>
+        </div>
+        ${index === 0 ? (`
+        <div class="keyboard">
+            <p>or type <span class="keyboard__key">y</span> or <span class="keyboard__key">n</span></p>
+        </div>`) : ''}
+    </div>
+`;
+
+export const AnswerTemplate = (index, total, question, answers) => `
+    <div class="answer">
+        <svg viewBox="0 0 36 36" class="progress-circle">
+            <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" class="progress-circle__text">${index}/${total}</text>
+            <path d="M18 2 a 16 16 0 0 1 0 32 a 16 16 0 0 1 0 -32" class="progress-circle__background"></path>
+            <path d="M18 2 a 16 16 0 0 1 0 32 a 16 16 0 0 1 0 -32" class="progress-circle__completed" style="stroke-dasharray: ${Math.round(index*100/total)}px, 100px;"></path>
+        </svg>
+        <h2 class="answer__title">${question}</h2>
+        <div class="answer__options">
+            ${answers.map(answer => `<div class="${answer['selected'] ? 'answer__option--selected' : 'answer__option'}" style="background-color: ${answer['background']}">
+                    <img class="answer__option__logo" src="${answer['logo']}" />
+                    <p class="answer__option__content" style="color: ${answer['color']}">${answer['title']}</p>
+                </div>`)}
+        </div>
+        <button class="answer__action" onclick="document.dispatchEvent(new Event('questionNext'))">Next</button>
+    </div>
+`;

          
A => src/lib/error.js +8 -0
@@ 0,0 1,8 @@ 
+class ValidationError extends Error {
+    constructor(message) {
+        super(message);
+        this.name = 'ValidationError';
+    }
+};
+
+export default ValidationError;

          
A => src/styles.scss +98 -0
@@ 0,0 1,98 @@ 
+.question, .answer {
+    min-height: 100%;
+    padding: 1rem;
+    display: flex;
+    position: relative;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+.question__title,
+.answer__title {
+    margin-top: 1rem;
+    margin-bottom: 0;
+    text-align: center;
+    min-height: calc(75px + 1rem);
+    max-width: 90%;
+    display: flex;
+    align-items: center;
+}
+
+.question__action,
+.answer__action {
+    margin-top: 2rem;
+    border: none;
+    height: 50px;
+    border-radius: 25px;
+    font-size: 20px;
+    padding: 0 2rem;
+    color: #fff;
+    background-color: #3c689b;
+    border: 1px solid #3c689b;
+    cursor: pointer;
+}
+.question__action:first-child {
+    margin-right: 0.5rem;
+}
+.question__action:last-child {
+    margin-left: 0.5rem;
+}
+
+.answer__options {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+}
+.answer__option {
+    padding: 2rem 0.5rem 0;
+    text-align: center;
+    flex: 0 1 calc(50% - 2rem);
+    border-radius: 5px;
+}
+.answer__option__logo {
+    width: 100%;
+}
+.answer__option__content {
+    line-height: 180%;
+    font-size: 16px;
+    font-smooth: grayscale;
+}
+
+.progress-circle {
+    position: absolute;
+    top: 1.5rem;
+    right: 4rem;
+    display: inline-block;
+    height: 40px;
+}
+.progress-circle__text {
+    fill: inherit;
+    font-size: 12px;
+}
+.progress-circle__background {
+    fill: none;
+    stroke: #eef9ff;
+    transition: stroke 250ms ease-in-out;
+    stroke-width: 3.8;
+}
+.progress-circle__completed {
+    stroke: #3c689b;
+    fill: none;
+    stroke-width: 3.8;
+    stroke-linecap: round;
+    stroke-dasharray: 0,100;
+    transition: stroke-dasharray .25s ease-in-out;
+}
+
+.keyboard {
+    margin-top: 1rem;
+}
+.keyboard__key {
+    display: inline-block;
+    border: 1px solid #ccdce6;
+    border-radius: 3px;
+    padding: 0.3rem 0.5rem;
+    box-shadow: 1px 1px 1px 1px rgba(113, 113, 113, 0.12);
+    background-color: inherit;
+}
  No newline at end of file

          
A => webpack.config.js +59 -0
@@ 0,0 1,59 @@ 
+const path = require('path');
+const webpack = require('webpack');
+const HTMLWebpackPlugin = require('html-webpack-plugin');
+const config = require('config');
+
+/*-------------------------------------------------*/
+
+module.exports = {
+    // webpack optimization mode
+    mode: ( process.env.NODE_ENV ? process.env.NODE_ENV : 'development' ),
+
+    // entry file(s)
+    entry: './src/index.js',
+
+    // output file(s) and chunks
+    output: {
+        library: 'ComparisonTable',
+        libraryTarget: 'umd',
+        globalObject: '(typeof self !== "undefined" ? self : this)',
+        libraryExport: 'default',
+        path: path.resolve(__dirname, 'dist'),
+        filename: 'index.js',
+        publicPath: config.get('publicPath')
+    },
+
+    // module/loaders configuration
+    module: {
+        rules: [
+            {
+                test: /\.js$/,
+                exclude: /node_modules/,
+                use: ['babel-loader']
+            },
+            {
+                test: /\.scss$/,
+                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
+            }
+        ]
+    },
+
+    plugins: [
+        new HTMLWebpackPlugin({
+            template: path.resolve(__dirname, 'index.html')
+        })
+    ],
+
+    // development server configuration
+    devServer: {
+
+        // must be `true` for SPAs
+        historyApiFallback: true,
+
+        // open browser on server start
+        open: config.get('open')
+    },
+
+    // generate source map
+    devtool: ( 'production' === process.env.NODE_ENV ? 'source-map' : 'cheap-module-eval-source-map' ),
+};