Skip to content

Instantly share code, notes, and snippets.

@SLeitgeb
Last active September 26, 2019 11:22
Show Gist options
  • Save SLeitgeb/5ae8d55a08f44b8fa04e763b0c30ab39 to your computer and use it in GitHub Desktop.
Save SLeitgeb/5ae8d55a08f44b8fa04e763b0c30ab39 to your computer and use it in GitHub Desktop.
Electrical safety test

javascript-quiz-library

Very simple JS library for quiz creation

Usage

git clone and edit questions.js. See index.html to change the quiz name.

Demo

See demo here.

body {
color: #36393D;
font-family: "Open Sans", Arial, sans-serif;
font-size: 90%;
margin: 1em auto;
width: 750px;
}
fieldset {
border-color: #356AA0;
margin-bottom: 1em;
border-width: 2px;
}
legend {
font-size: 105%;
font-weight: 600;
padding-left: 15px;
padding-right: 15px;
padding-top: 15px;
}
label {
display: block;
line-height: 1.75em;
}
input[type="radio"] {
margin-right: 10px;
}
input[type="submit"] {
background: #689DD3;
border: 1px solid #356AA0;
color: white;
display: block;
font-size: 120%;
font-weight: 600;
height: 2.5em;
margin-top: 2em;
text-transform: uppercase;
width: 100%;
}
table {
color: white;
font-weight: bold;
margin: 1em auto 2em auto;
width: 360px;
}
td {
padding: 5px 15px;
text-align: left;
width: 60px;
}
td.missing-label,
td.right-label,
td.wrong-label {
border-bottom: 1px solid;
border-left: 1px solid;
border-top: 1px solid;
}
td.missing-score,
td.right-score,
td.wrong-score {
border-bottom: 1px solid;
border-right: 1px solid;
border-top: 1px solid;
text-align: right;
}
td.missing-label,
td.missing-score {
background: #C79810;
border-bottom-color: #946500;
border-left-color: #946500;
border-top-color: #946500;
}
td.right-label,
td.right-score {
background: #6BBA70;
border-bottom-color: #38873D;
border-top-color: #38873D;
}
td.wrong-label,
td.wrong-score {
background: #D01F3C;
border-bottom-color: #9D0009;
border-right-color: #9D0009;
border-top-color: #9D0009;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Elektrotechnická kvalifikace test</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.css">
<link rel="stylesheet" type="text/css" href="form.css">
<link rel="stylesheet" type="text/css" media="print" href="print.css">
</head>
<body>
<script src="Result.js"></script>
<script src="Option.js"></script>
<script src="Quiz.js"></script>
<script src="Question.js"></script>
<script src="questions.js"></script>
<script>
var quiz = new Quiz("Elektrotechnická kvalifikace test", questions, {"shuffle": true, "shuffle_answers": true});
</script>
</body>
</html>
"use strict";
/**
* @param {string} option value
* @param {string} Question uid
*/
function Option(value, uid) {
this.value = value;
this.uid = uid;
this.checked = false;
}
/**
* Renders HTML.
* @return {DOM Element}
*/
Option.prototype.render = function() {
var label = document.createElement("label"),
option = document.createElement("input"),
self = this;
option.value = this.value;
option.type = "radio";
option.name = this.uid;
option.addEventListener("change", function(e) {
self.checked = this.checked;
});
label.appendChild(option);
label.appendChild(document.createTextNode(option.value));
return label;
};
/**
* @return {string}
*/
Option.prototype.getValue = function() {
return this.value;
};
/**
* @return {boolean}
* ugly
*/
Option.prototype.isSelected = function() {
var choice = document.querySelector("input[name='" + this.uid + "']:checked");
return choice ? choice.value == this.value : false;
};
* {
border-color: black;
color: black;
}
fieldset {
border-color: black;
}
input[type="radio"] {
page-break-after: avoid;
page-break-before: avoid;
}
input[type="submit"] {
display: none;
}
"use strict";
/**
* @param {string} question to ask
* @param {array} options to choose from
* @param {integer} right option array index
*/
function Question(question, options, rightOption) {
this.question = question;
this.rightOption = rightOption;
this.uid = this._uid();
this.options = this._createOptions(options);
this.RNDR_RIGHT = "#6BBA70",
this.RNDR_WRONG = "#D01F3C",
this.RNDR_MISSING = "#C79810";
}
/**
* @return {string} question id
*/
Question.prototype._uid = function() {
// https://stackoverflow.com/questions/3242324/javascript-dateobj-gettime-for-a-uid-is-the-length-not-fixed
return "q" + Math.random().toString(36).substr(2,9);
};
/**
* @param {array} options
* @return {array} of Option objects
*/
Question.prototype._createOptions = function(_options) {
var options = [];
for (var i = 0; i < _options.length; i += 1) {
options[i] = new Option(_options[i], this.uid);
}
return options;
}
/**
* @return {array}
*/
Question.prototype.getOptions = function() {
return this.options;
};
/**
* @return {DOM Element}
*/
Question.prototype.render = function(shuffle) {
var fieldset = document.createElement("fieldset"),
legend = document.createElement("legend"),
self = this;
fieldset.id = this.uid;
if (shuffle) {
this._shuffleOptions();
}
legend.innerHTML = this.question;
fieldset.appendChild(legend);
for (var i = 0; i < this.options.length; i += 1) {
fieldset.appendChild(this.options[i].render());
}
return fieldset;
};
Question.prototype.renderMissing = function() {
document.getElementById(this.uid).style.borderColor = this.RNDR_MISSING;
};
Question.prototype.renderRight = function(first_argument) {
document.getElementById(this.uid).style.borderColor = this.RNDR_RIGHT;
};
Question.prototype.renderWrong = function() {
document.getElementById(this.uid).style.borderColor = this.RNDR_WRONG;
};
/**
* @param {Option}
* @return {boolean}
*/
Question.prototype.isRight = function(option) {
return option.getValue() == this.options[this.rightOption].value;
};
/**
* Shuffles the options.
*/
Question.prototype._shuffleOptions = function() {
var result = [],
rightOption = this.options[this.rightOption].value; // obtain right option
while (this.options.length) {
var len = this.options.length,
idx = parseInt(Math.random() * len); // find an index
result.push(this.options.splice(idx, 1)[0]); // remove that item from array
if (result[result.length - 1].value == rightOption) {
this.rightOption = result.length - 1; //
}
}
this.options = result; // switch arrays
}
"use strict";
let questions = [
new Question("Rozdělení EZ podle napětí mezi vodiči v uzemněné střídavé soustavě je:", ["mn do 50 V; nn do 1000 V; vn do 52 kV", "mn do 55 V; nn do 1500 V; vn do 73 kV", "pn do 50 V; dn do 1000 V; sn do 52 kV"], 0),
new Question("Jmenovité napětí střídavých rozvodných sítí v ČR je:", ["230/400 V","300/450 V", "120/230 V" ], 0),
new Question("Mez bezpečných jmenovitých napětí při předpokládaném dotyku neživých částí v prostorech normálních je:", ["50 V~ a 120 V=", "40 V~ a 115 V=", "100 V~ a 85 V="], 0),
new Question("Dovolené meze trvalého dotykového napětí v prostorech normálních i nebezpečných pro EZ do 1000 V jsou:", ["25 V~ a 60 V=", "20 V~ a 67 V=", "50 V~ a 42 V="], 0),
new Question("Odpor uzemnění uzlu zdroje nemá být větší než:", ["5 Ω, ve ztížených podmínkách nejvýše 15 Ω", "6 Ω, ve ztížených podmínkách nejvýše 18 Ω", "2 Ω, ve ztížených podmínkách nejvýše 8 Ω" ], 0),
new Question("Plynové potrubí se jako náhodný ochranný vodič:", ["nesmí použít", "smí použít"], 0),
new Question("Silnoproudá zařízení jsou zařízení:", ["u nichž mohou vzniknout proudy nebezpečné osobám, zvířatům a věcem", "u nichž mohou vzniknout proudy nebezpečné rostlinám", "u nichž nemusí vzniknout proudy nebezpečné osobám, zvířatům a věcem"], 0),
new Question("Obecná charakteristika prostorů nebezpečných říká, že:", ["vlivem prostředí je zde stálé nebo přechodné nebezpečí úrazu elektrickým proudem", "vlivem lidských chyb je zde stálé nebo přechodné nebezpečí úrazu elektrickým proudem"], 0),
new Question("Z hlediska možného zvýšení rizika úrazu elektrickým proudem působením vnějších vlivů se prostory člení na:", ["normální, nebezpečné a zvlášť nebezpečné", "bezpečné, normální a nebezpečné","bezpečné a nebezpečné" ], 0),
new Question("Barva bezpečnostních tabulek výstrahy je:", ["žlutá", "červená", "zelená"], 0),
new Question("Barva bezpečnostních tabulek zákazu je:", ["červená", "fialová", "žlutá"], 0),
new Question("Bezpečnostní značka v provedení modrý kruh, bílý symbol je značka:", ["příkazu", "informační", "rozkazu"], 0),
new Question("Bezpečnostní značky v provedení na zeleném podkladu jsou značky:", ["informační", "doporučovací", "příkazu"], 0),
new Question("V označení stupně krytí IP udává první číslice:", ["stupeň ochrany osob před nebezpečným dotykem živých částí a stupeň ochrany zařízení před vniknutím cizích pevných těles", "stupeň ochrany osob před nebezpečným dotykem živých částí a stupeň ochrany zařízení před vniknutím cizích tekutých těles", "stupeň ochrany osob před nebezpečným dotykem neživých částí a stupeň ochrany zařízení před vniknutím cizích pevných těles"], 0),
new Question("Prostředky základní ochrany (před přímým dotykem) EZ zajišťují ochranu:", ["jen při normálním provozu", "při normálním a speciálním provozu provozu", "jen při speciálním provozu"], 0),
new Question("Prostředky zvýšené ochrany (před přímým i nepřímým dotykem) EZ zajišťují ochranu:", ["základní i při poruše", "jen při normálním provozu", "jen při poruše"], 0),
new Question("Při ochraně automatickým odpojením se pro střídavé rozvodné sítě do 1000 V dovoluje doba odpojení:", ["nepřesahující 5 s", "přesahující 5 s", "nepřesahující 3 s"], 0),
new Question("Pro automatické odpojení od zdroje v síti TT 230 V AC, zajišťující ochranu před nepřímým dotykem koncového obvodu do 32 A, musí ochranný prvek vypnout v čase do:", ["0,2 s.", "0,4 s.", "0,6 s."], 0),
new Question("Pro automatické odpojení od zdroje v síti TN 230 V AC, zajišťující ochranu před nepřímým dotykem koncového obvodu do 32 A, musí ochranný prvek vypnout v čase do:", ["0,4 s.", "0,2 s.", "0,6 s."], 0),
new Question("Při základní ochraně krytím musí kryty:", ["být dostatečně odolné, vyhovující alespoň IPxxB (IP2X), horní vodorovné kryty pak IPxxD (IP4X), odstranitelné jen klíčem či nástrojem", "být dostatečně odolné", "být dostatečně odolné, vyhovující alespoň IPxxC (IP3X), horní vodorovné kryty pak IPxxG (IP6X),"], 0),
new Question("Ochranu zábranou v prostorech volně přístupných laikům:", ["nelze použít", "lze použít"], 0),
new Question("Napětí obvodů při ochranně elektrickým oddělením nesmí přesáhnout hodnotu:", ["500 V", "1000 V", "5000 V"], 0),
new Question("Základní ochrana polohou spočívá:", ["v umístění živých částí mimo dosah", "v umístění neživých částí mimo dosah", "v umístění živých částí za neživé části"], 0),
new Question("Doplňkovou ochranu proudovým chráničem lze realizovat:", ["chráničem se jmenovitým rozdílovým proudem nejvýše 30 mA", "chráničem se jmenovitým rozdílovým proudem nejvýše 60 mA", "chráničem se jmenovitým rozdílovým proudem nejvýše 15 mA"], 0),
new Question("Proudové chrániče s rozdílovým proudem do 30 mA se musí požít na:", ["zásuvky se jmenovitým proudem do 32 A, které jsou používány laiky.", "zásuvky se jmenovitým proudem do 32 A, které jsou používány specialisty.", "zásuvky se jmenovitým proudem do 42 A, které jsou používány laiky."], 0),
new Question("Pro ochranu omezením ustáleného dotykového proudu a energie nesmí proud tekoucí odporem 2000 Ohmů mezi částmi současně přístupnými dotyku překročit hodnoty:", ["3,5 mA~ nebo 10 mA=", "10 mA~ nebo 3,5 mA=", "4,5 mA~ nebo 12 mA="], 0),
new Question("Pro ochranu omezením ustáleného proudu a energie nesmí náboj mezi částmi současně přístupnými dotyku překročit hodnotu:", ["50 μC", "80 μC", "100 μC"], 0),
new Question("Proudovým chráničem musí procházet:", ["všechny pracovní vodiče", "nulový vodič", "zemění"], 0),
new Question("Vidlice a zásuvky pro obvody SELV musí splňovat tyto požadavky:", ["nesmí být záměnné s vidlicemi a zásuvkami sítí jiných napětí a nesmí mít kontakt pro ochranný vodič", "nesmí být záměnné s vidlicemi a zásuvkami sítí jiných napětí a musí mít kontakt pro ochranný vodič"], 0),
new Question("Živé části obvodů PELV s napětím nepřesahujícím 12V~ nebo 25V=:", ["nemusí mít základní ochranu", "musí mít základní ochranu", "musí mít zvýšenou ochranu"], 0),
new Question("Jako zdroje pro ochranu malým napětím lze použít:", ["bezpečnostní ochranný transformátor, akumulátor", "bezpečnostní ochranný rezistor"], 0),
new Question("Obvody SELV:", ["nesmí být spojeny se zemí", "jsou spojeny se zemí"], 0),
new Question("Z označení PELV vyplývá, že jde o obvody:", ["uzemněné", "neuzemněné"], 0),
new Question("Ochrana základní izolací znamená, že:", ["živé části musí být úplně pokryty izolací, kterou lze odstranit pouze jejím zničením", "živé části musí být úplně pokryty dvojitou izolací, kterou nelze odstranit"], 0),
new Question("Zařízení mající dvojitou či zesílenou izolaci se označují jako:", ["EZ třídy ochrany II", "EZ třídy ochrany I", "EZ třídy ochrany III"], 0),
new Question("Elektrické zařízení třídy ochrany II:", ["má dvojitou či zesílenou izolaci a nemá svorku pro připojení ochranného vodiče", "má dvojitou či zesílenou izolaci a má svorku pro připojení ochranného vodiče", "nemá dvojitou či zesílenou izolaci a nemá svorku pro připojení ochranného vodiče"], 0),
new Question("Elektrické zařízení, u něhož je provedena jen pracovní izolace, se považuje z hlediska možného úrazu elektrickým proudem za zařízení:", ["bez ochrany", "se základní ochranou", "se zvýšenou ochranou"], 0),
new Question("V sítích TT se neživé části EZ:", ["nesmějí připojit na nulový vodič", "musí připojit na nulový vodič"], 0),
new Question("Elektrická zařízení třídy ochrany II musí mít připojovací vidlici:", ["bez ochranného vodiče", "s ochranným vodičem"], 0),
new Question("Elektrická zařízení třídy ochrany 0:", ["nelze v ČR používat", "jsou zařízení s pracovní izolací"], 0),
new Question("Izolace, která slouží k ochraně při poruše (před nepřímým dotykem) se nazývá:", ["přídavná", "dodatečná", "zvýšená"], 0),
new Question("Pro připojení elektrických předmětů třídy ochrany I se dvouvodičový pohyblivý přívod s vidlicí bez ochranného kontaktu:", ["nesmí použít", "smí použít"], 0),
new Question("Jednofázový pohyblivý prodlužovací přívod pro obecné použití se provede jako:", ["třížilový, přičemž ochranný vodič je veden samostatně a nesmí být spojen s nulovým vodičem", "dvoužilový, přičemž ochranný vodič je veden samostatně a nesmí být spojen s nulovým vodičem", "třížilový, přičemž ochranný vodič musí být spojen s nulovým vodičem"], 0),
new Question("Barva izolovaných krajních (fázových) vodičů může být:", ["černá, hnědá nebo šedá", "černá, žlutá, zelená", "červená, zelená, černá"], 0),
new Question("Fázové svorky elektrických předmětů a zařízení se ve střídavé rozvodné soustavě označují písmeny:", ["U, V, W", "X, Y, Z", "P, Q, R"], 0),
new Question("Nulový (střední) vodič střídavé soustavy se označuje:", ["N", "S", "T"], 0),
new Question("Svorka ochranného vodiče se na elektrickém předmětu značí:", ["PE", "PEN", "P"], 0),
new Question("Vodič PEN je:", ["vodič slučující funkci ochranného vodiče a nulového vodiče", "ochraný vodič"], 0),
new Question("Jištění vodičů PE a PEN:", ["je zakázáno", "je povinný"], 0),
new Question("Obsluha je taková činnost na EZ, kdy:", ["pracovník nepoužívá nástrojů a nemůže přijít do styku s živými částmi", "pracovník používá nástrojů a nemůže přijít do styku s živými částmi", "pracovník nepoužívá nástrojů a může přijít do styku s živými částmi"], 0),
new Question("Práce je taková činnost na EZ, kdy:", ["pracovník používá nástrojů a může přijít do styku s živými částmi", "pracovník nepoužívá nástrojů a nemůže přijít do styku s živými částmi", "pracovník používá nástrojů a nemůže přijít do styku s živými částmi"], 0),
new Question("§4 Vyhlášky 50/1978 Sb. stanovuje požadavky pro kvalifikaci:", ["pracovníků poučených", "pracovníků znalých", "pracovníků studujících"], 0),
new Question("§5 Vyhlášky 50/1978 Sb. stanovuje požadavky pro kvalifikaci:", ["pracovníků znalých", "pracovníků poučených", "pracovníků studujících"], 0),
new Question("Pracovníci poučení mohou na EZ pracovat:", ["v blízkosti nekrytých částí EZ nn pod napětím ve vzdálenosti větší než 20 cm s dohledem", "v blízkosti nekrytých částí EZ nn pod napětím ve vzdálenosti větší než 30 cm s dohledem", "v blízkosti nekrytých částí EZ nn pod napětím ve vzdálenosti větší než 20 cm bez dohledu"], 0),
new Question("Při práci pod dozorem odpovídá za dodržování bezpečnostních předpisů:", ["pracovník dozírající", "pracovník provádějící činnost"], 0),
new Question("Při práci s dohledem odpovídá za dodržování bezpečnostních předpisů:", ["pracovník provádějící činnost", "pracovník dohlížející"], 0),
new Question("Pro následky úrazu elektrickým proudem je rozhodující:", ["velikost proudu, který protéká tělem zasaženého", "velikost napětí, který protéká tělem zasaženého"], 0),
new Question("Za současně přístupné dotyku se považují části navzájem vzdálené:", ["méně než 2,5 m", "méně než 3 m", "méně než 2 m"], 0),
new Question("Pracovními pomůckami jsou například:", ["měřicí přístroje, zkoušečky napětí a další předměty potřebné k práci či obsluze EZ", "ochranné izolační rukavice, izolační koberce a plošiny, ochranné brýle apod."], 0),
new Question("Ochrannými pomůckami jsou například:", ["ochranné izolační rukavice, izolační koberce a plošiny, ochranné brýle apod.", "měřicí přístroje, zkoušečky napětí a další předměty potřebné k práci či obsluze EZ"], 0),
new Question("Před použitím ochranných pomůcek je pracovník povinen:", ["přesvědčit se o jejich řádném stavu", "vyčistit je", "otestovat je na zařízení pod proudem"], 0),
new Question("Nepřímá srdeční masáž má být prováděna s frekvencí asi:", ["100 stlačení za minutu", "80 stlačení za minutu", "110 stlačení za minutu"], 0),
new Question("Před započetím nepřímé srdeční masáže je potřeba:", ["uložit postiženého rovně na záda na rovnou a pevnou podložku", "uložit postiženého na postel", "uložit postiženého do stabilizované polohy"], 0),
new Question("Při provádění nepřímé srdeční masáže musí mít zachránce:", ["obě ruce napnuté v loktech", "obě ruce pokrčené v loktech"], 0),
new Question("Při provádění nepřímé srdeční masáže je nutno stlačovat hrudní kost u dospělého člověka:", ["do hloubky 4-5 cm", "do hloubky 5-6 cm", "do hloubky 3-4 cm"], 0),
new Question("Poměr stlačení srdce a umělého dýchání při resuscitaci má být:", ["30 stlačení a 2 vdechy", "40 stlačení a 3 vdechy", "30 stlačení a 1 vdech" ], 0),
new Question("Automatický externí defibrilátor použijeme v případě:", ["zástavy dechu postiženého", "zástavy srdce postiženého"], 0),
new Question("Pokud postižený po úrazu elektrickým proudem dýchá a má hmatný tep, zůstává při vědomí a není viditelně zraněn:", ["je nutno přesto zavolat lékařskou pomoc", "je nutné mu podat vodu", "uložíme ho ke spánku"], 0),
new Question("Záchrannou lékařskou službu je možné přivolat tísňovým telefonním číslem:", ["155", "158", "150"], 0),
new Question("Jestliže postižený nabyl po resuscitaci vědomí:", ["musí být nadále sledován a musí zůstat vleže", "posadíme ho", "necháme ho usnout"], 0),
new Question("Resuscitace se musí provádět:", ["dokud se neobnoví životní funkce nebo do předání postiženého do péče záchranářů", "dokud nás to baví"], 0)
];
"use strict";
/**
* Quiz
* @param {string} name
* @param {array} questions
* @param {object} options
*
* options object supports these keys:
* shuffle {boolean} should the questions be shuffled before rendered?
* shuffle_answers {boolean} should the answers be shuffled before rendered?
*/
function Quiz(name, questions, options) {
this.name = name;
this.questions = questions;
this.options = options || {};
if (this.options.shuffle) this.shuffleQuestions();
this.create();
}
Quiz.prototype.getName = function() {
return this.name;
};
Quiz.prototype.setName = function(name) {
this.name = name;
return this.name;
};
Quiz.prototype.getQuestions = function() {
return this.questions;
};
Quiz.prototype.setQuestions = function(questions) {
this.questions = questions;
return this.questions;
};
Quiz.prototype.shuffleQuestions = function() {
for(let i = this.questions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * i);
const temp = this.questions[i];
this.questions[i] = this.questions[j];
this.questions[j] = temp;
}
return this.questions;
};
Quiz.prototype.addQuestion = function(question) {
return this.questions.push(question);
};
Quiz.prototype.removeQuestion = function(question) {
console.info("Remove question from the quiz");
return this.questions;
};
Quiz.prototype.create = function() {
let form = document.createElement("form"),
submit = document.createElement("input"),
self = this;
document.title = this.name;
document.write("<h1>" + this.name + "</h1>");
submit.type = "submit";
submit.value = "Submit quiz";
form.addEventListener("submit", function(e) {
e.preventDefault();
self.submit();
})
for (let i in this.questions) {
form.appendChild(this.questions[i].render(this.options.shuffle_answers || false));
}
form.appendChild(submit);
document.body.appendChild(form);
};
Quiz.prototype.submit = function() {
let missing = 0,
right = 0,
wrong = 0;
for (let q of this.getQuestions()) {
let choice = null;
// validate
for (let o of q.getOptions()) {
if (o.isSelected()) {
choice = o;
}
}
// evaluate
if (!choice) { // skip evaluation
q.renderMissing();
missing += 1;
continue;
}
if (!q.isRight(choice)) {
q.renderWrong();
wrong += 1;
} else {
q.renderRight();
right += 1;
}
}
// show result
let result = new Result(missing, right, wrong).render();
};
"use strict";
/**
* @param {integer} missing
* @param {integer} right
* @param {integer} wrong
*/
function Result(missing, right, wrong) {
this.html = document.createElement("table");
var labels = ["Missing", "Right", "Wrong"],
tr = document.createElement("tr");
for (var i = 0; i < arguments.length; i += 1) {
var label = document.createElement("td"),
score = document.createElement("td");
label.className = labels[i].toLowerCase() + "-label";
score.className = labels[i].toLowerCase() + "-score";
label.innerHTML = labels[i];
score.innerHTML = arguments[i];
tr.appendChild(label);
tr.appendChild(score);
}
this.html.appendChild(tr);
}
Result.prototype.render = function() {
var old = document.getElementsByTagName("table")[0];
if (old) {
document.body.removeChild(old);
}
document.body.appendChild(this.html);
return this;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment