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' ),
+};