diff --git a/Week1/homework/js-exercises/common-files/api-axios.js b/Week1/homework/js-exercises/common-files/api-axios.js
new file mode 100644
index 000000000..adf85c0b9
--- /dev/null
+++ b/Week1/homework/js-exercises/common-files/api-axios.js
@@ -0,0 +1,17 @@
+
+ function requestByAxios (theURL,onSuccess,onFailure) {
+ axios.get(theURL)
+ .then(function(aResponse) {
+ // console.log('Axios call success! ');
+ // console.log(aResponse);
+ onSuccess(aResponse.data);
+ })
+ .catch(function(anError) {
+ // console.log('Axios call failed with following message: ');
+ // console.log(anError);
+ onFailure('Axios',undefined,anError);
+ });
+ };
+
+;
+
diff --git a/Week1/homework/js-exercises/common-files/api-xhr.js b/Week1/homework/js-exercises/common-files/api-xhr.js
new file mode 100644
index 000000000..5ca0ce102
--- /dev/null
+++ b/Week1/homework/js-exercises/common-files/api-xhr.js
@@ -0,0 +1,22 @@
+
+ function requestByXHR (theURL,onSuccess,onFailure) {
+ let myXHR=new XMLHttpRequest();
+ myXHR.onreadystatechange=function() {
+ if (myXHR.readyState===XMLHttpRequest.DONE){
+ if (myXHR.status===200) {
+ // console.log('XHR call success! ');
+ // console.log(JSON.parse(myXHR.responseText));
+ onSuccess(JSON.parse(myXHR.responseText));
+ } else {
+ // console.log(`XHR call failed with status=[${myXHR.status}] `);
+ // if (myXHR.statusText) {console.log(myXHR.statusText)};
+ onFailure('XHR',myXHR.status,myXHR.statusText);
+ };
+ };
+ };
+ myXHR.open("GET",theURL,true);
+ myXHR.send();
+ };
+
+;
+
diff --git a/Week1/homework/js-exercises/ex01-random-user/index.html b/Week1/homework/js-exercises/ex01-random-user/index.html
new file mode 100644
index 000000000..401112b91
--- /dev/null
+++ b/Week1/homework/js-exercises/ex01-random-user/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Request new Data through XHR
+
+
+
+
Request new Data through Axios
+
+
+
+
+
+
+
+
+
diff --git a/Week1/homework/js-exercises/ex01-random-user/main.css b/Week1/homework/js-exercises/ex01-random-user/main.css
new file mode 100644
index 000000000..489920058
--- /dev/null
+++ b/Week1/homework/js-exercises/ex01-random-user/main.css
@@ -0,0 +1,67 @@
+
+* {
+ box-sizing: border-box;
+ margin: 0px;
+ padding: 0px;
+}
+
+body {
+ margin: 0px 18px;
+ font-family: sans-serif;
+ font-size: 1.5em;
+}
+
+table {
+ padding: 0px 0px 10px;
+}
+
+td {
+ padding: 0px 12px 3px 0px;
+}
+
+hr {
+ display: block;
+ margin: 12px auto 12px;
+ color: navy;
+ border-style: outset;
+ border-width: 2px;
+}
+
+.cls_text_align_left {
+ text-align: left;
+}
+
+.cls_text_align_right {
+ text-align: right;
+}
+
+.cls_text_align_center {
+ text-align: center;
+}
+
+.cls_unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+#id_page_header {
+ padding: 12px 16px 8px 16px;
+ margin: 20px 0 0;
+}
+
+.cls_api_data_container {
+ background: linear-gradient(to bottom right, orange, gold);
+ border-radius: 12px;
+ font-size: 1.2rem;
+ padding: 20px;
+ margin-top: 15px;
+ margin-left: 10px;
+ display: inline-block;
+}
+
+;
+
diff --git a/Week1/homework/js-exercises/ex01-random-user/main.js b/Week1/homework/js-exercises/ex01-random-user/main.js
new file mode 100644
index 000000000..2c32e0ca9
--- /dev/null
+++ b/Week1/homework/js-exercises/ex01-random-user/main.js
@@ -0,0 +1,60 @@
+
+{
+ 'use strict';
+
+ let lmntRef=document.getElementById('id_page_header');
+ lmntRef.style.backgroundColor='gold';
+ document.getElementById('id_page_title').innerHTML=lmntRef.innerHTML=
+ `(Homework: Javascript 3 - week 1) - (Exercise 01: randomuser API with XHR & Axios)`;
+ lmntRef.style.borderRadius='12px';
+ document.body.style.backgroundColor='#102030';
+
+ const symbolEuro='€';
+ const symbolApostrophe='’'; /* ’ or ´ or ‘ */
+
+ const xhrListRef=document.getElementById('id_list_xhr_data');
+ const axiosListRef=document.getElementById('id_list_axios_data');
+ const targetURL='https://www.randomuser.me/api';
+
+ apiCallThroughXHR();
+ apiCallThroughAxios();
+
+ function apiCallThroughXHR(){
+ xhrListRef.innerHTML=createProgressHTML();
+ requestByXHR(targetURL,(apiData)=>{xhrListRef.innerHTML=createDataHTML(apiData.results[0])}
+ ,(apiMethod,errorStatus,errorMsg)=>{xhrListRef.innerHTML=createErrorHTML(errorStatus,errorMsg)});
+ };
+
+ function apiCallThroughAxios() {
+ axiosListRef.innerHTML=createProgressHTML();
+ requestByAxios(targetURL,(apiData)=>{axiosListRef.innerHTML=createDataHTML(apiData.results[0])}
+ ,(apiMethod,errorStatus,errorMsg)=>{axiosListRef.innerHTML=createErrorHTML(errorStatus,errorMsg)});
+ };
+
+ function createDataHTML(theData) {
+ return `Name (${theData.name.title}) ${
+ theData.name.first} ${theData.name.last} `
+ + `Gender ${theData.gender} `
+ + `Born ${theData.dob.date.substr(0,10)} `
+ + `Age ${theData.dob.age} `
+ + `Email ${theData.email} `
+ + `Country ${theData.location.country} `
+ + `City ${theData.location.city} `;
+ }
+
+ function createErrorHTML(theStatus,theMessage) {
+ return 'Result Unsuccessful '
+ + ' '
+ + `Status code ${theStatus} `
+ + ' '
+ + `Message ${theMessage} `;
+ }
+
+ function createProgressHTML()
+ {return 'Request Sending in progress... ';}
+
+}
+
+
+;
+
diff --git a/Week1/homework/js-exercises/ex02-random-humor/index.html b/Week1/homework/js-exercises/ex02-random-humor/index.html
new file mode 100644
index 000000000..e8740c1bd
--- /dev/null
+++ b/Week1/homework/js-exercises/ex02-random-humor/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Request new Data through XHR
+
Request new Data through Axios
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week1/homework/js-exercises/ex02-random-humor/main.css b/Week1/homework/js-exercises/ex02-random-humor/main.css
new file mode 100644
index 000000000..7feef5b12
--- /dev/null
+++ b/Week1/homework/js-exercises/ex02-random-humor/main.css
@@ -0,0 +1,87 @@
+
+* {
+ box-sizing: border-box;
+ margin: 0px;
+ padding: 0px;
+}
+
+body {
+ margin: 0px 18px;
+ font-family: sans-serif;
+ font-size: 1.5em;
+}
+
+table {
+ border-spacing: 0;
+}
+
+tr {
+ background: linear-gradient(to bottom right, gold, orange);
+}
+
+td {
+ vertical-align: top;
+ padding: 1px 4px;
+ max-width: 58vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+img {
+ margin: 0 10px 10px 0;
+ height: 100%;
+ border-width: 2px;
+ border-style: outset;
+}
+
+hr {
+ display: block;
+ margin: 12px auto 12px;
+ color: navy;
+ border-style: outset;
+ border-width: 2px;
+}
+
+.cls_text_align_left {
+ text-align: left;
+}
+
+.cls_text_align_right {
+ text-align: right;
+}
+
+.cls_text_align_center {
+ text-align: center;
+}
+
+.cls_unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.cls_page_header {
+ padding: 12px 16px 8px 16px;
+ margin: 20px 0px;
+}
+
+.cls_api_container {
+ background-color: orange;
+ font-size: 1.2rem;
+ padding: 12px 16px;
+ margin-top: 10px;
+ border-radius: 16px;
+}
+
+.cls_api_data_container {
+ padding-top: 10px;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+
+;
+
diff --git a/Week1/homework/js-exercises/ex02-random-humor/main.js b/Week1/homework/js-exercises/ex02-random-humor/main.js
new file mode 100644
index 000000000..1c900154a
--- /dev/null
+++ b/Week1/homework/js-exercises/ex02-random-humor/main.js
@@ -0,0 +1,84 @@
+
+{
+ 'use strict';
+
+ let lmntRef=document.getElementById('id_page_header');
+ lmntRef.style.backgroundColor='gold';
+ document.getElementById('id_page_title').innerHTML=lmntRef.innerHTML=
+ `(Homework: Javascript 3 - week 1) - (Exercise 02: Load image given by API with XHR & Axios)`;
+ lmntRef.style.borderRadius='12px';
+ document.body.style.backgroundColor='#102030';
+ lmntRef=document.getElementById('id_page_info');
+ lmntRef.style.backgroundColor='gold';
+ lmntRef.innerHTML='Using randomuser’s api for images since xkcd’s api is throwing CORB';
+ lmntRef.style.borderRadius='12px';
+
+ const symbolEuro='€';
+ const symbolApostrophe='’'; /* ’ or ´ or ‘ */
+
+ const dataTableRef=document.getElementById('id_api_data_table');
+ const dataImageRef=document.getElementById('id_api_data_image');
+
+ const targetURL='https://www.randomuser.me/api';
+
+ apiCallThroughXHR();
+
+ function apiCallThroughXHR(){
+ showRequestStart('XHR');
+ requestByXHR(targetURL,showRequestData,showRequestError);
+ };
+
+ function apiCallThroughAxios() {
+ showRequestStart('Axios');
+ requestByAxios(targetURL,showRequestData,showRequestError);
+ };
+
+ function showRequestStart(theMethod) {
+ resetInfo();
+ dataTableRef.innerHTML=
+ 'Request in progress... '
+ + ' '
+ + `from ${targetURL} `
+ + ' '
+ + `with ${theMethod} `;
+ };
+
+ function showRequestError(theMethod,theStatus,theMessage) {
+ resetInfo();
+ dataTableRef.innerHTML=
+ `Request Unsuccessful `
+ + ' '
+ + `from ${targetURL} `
+ + ' '
+ + `with ${theMethod} `
+ + ' '
+ + `Status code ${theStatus} `
+ + ' '
+ + `Message ${theMessage} `;
+ };
+
+ function showRequestData(theData) {
+ const myData=theData.results[0];
+ resetInfo();
+ dataTableRef.innerHTML=
+ `Name (${myData.name.title}) ${
+ myData.name.first} ${myData.name.last} `
+ + `Gender ${myData.gender} `
+ + `Born ${myData.dob.date.substr(0,10)} `
+ + `Age ${myData.dob.age} `
+ + `Email ${myData.email} `
+ + `Country ${myData.location.country} `
+ + `City ${myData.location.city} `;
+ dataImageRef.setAttribute('src',myData.picture.large);
+ dataImageRef.hidden=false;
+ };
+
+ function resetInfo() {
+ dataTableRef.innerHTML='';
+ dataImageRef.hidden=true;
+ };
+};
+
+
+;
+
diff --git a/Week1/homework/js-exercises/ex03-dog-photo-gallery/index.html b/Week1/homework/js-exercises/ex03-dog-photo-gallery/index.html
new file mode 100644
index 000000000..08675e679
--- /dev/null
+++ b/Week1/homework/js-exercises/ex03-dog-photo-gallery/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Next Image through XHR
+ Next Image through Axios
+ id_api_status_text
+
+
+
+
+
+
+
+
+
diff --git a/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.css b/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.css
new file mode 100644
index 000000000..589e4eda3
--- /dev/null
+++ b/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.css
@@ -0,0 +1,78 @@
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background-color: #102030;
+ font-size: 1.2rem;
+ color: gold;
+ margin: 0px 18px;
+ font-family: sans-serif;
+}
+
+ul {
+ list-style-type: none;
+ display: grid;
+ grid-template-columns:repeat(auto-fit,minmax(24rem,1fr));
+ grid-gap: 4px 6px;
+ transform: rotate(180deg);
+}
+
+li {
+ transform: rotate(180deg);
+ max-width: 84vmin;
+}
+
+img {
+ width: 100%;
+}
+
+hr {
+ display: block;
+ margin: 12px auto 12px;
+ color: navy;
+ border-style: outset;
+ border-width: 2px;
+}
+
+.cls_text_align_left {
+ text-align: left;
+}
+
+.cls_text_align_right {
+ text-align: right;
+}
+
+.cls_text_align_center {
+ text-align: center;
+}
+
+.cls_unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.cls_page_header {
+ background-color: gold;
+ font-size: 1.5em;
+ color: black;
+ border-radius: 12px;
+ margin: 20px 0px;
+ padding: 12px 16px 8px 16px;
+}
+
+#id_api_status_text {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+
+;
+
diff --git a/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.js b/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.js
new file mode 100644
index 000000000..b4594230f
--- /dev/null
+++ b/Week1/homework/js-exercises/ex03-dog-photo-gallery/main.js
@@ -0,0 +1,50 @@
+
+{
+ 'use strict';
+
+ let lmntRef=document.getElementById('id_page_header');
+ document.getElementById('id_page_title').innerHTML=lmntRef.innerHTML=
+ `(Homework: Javascript 3 - week 1) - (Exercise 03: Dog photo API with XHR & Axios)`;
+
+ const symbolEuro='€';
+ const symbolApostrophe='’'; /* ’ or ´ or ‘ */
+
+ const dataTextRef=document.getElementById('id_api_status_text');
+ const dataListRef=document.getElementById('id_api_data_list');
+
+ const targetURL='https://dog.ceo/api/breeds/image/random';
+
+ apiCallThroughXHR();
+
+ function apiCallThroughXHR(){
+ showRequestStart('XHR');
+ requestByXHR(targetURL,showRequestData,showRequestError);
+ };
+
+ function apiCallThroughAxios() {
+ showRequestStart('Axios');
+ requestByAxios(targetURL,showRequestData,showRequestError);
+ };
+
+ function showRequestStart(theMethod) {
+ dataTextRef.innerHTML=`API Request at ${targetURL} by ${theMethod}`;
+ };
+
+ function showRequestError(theMethod,theStatus,theMessage) {
+ dataTextRef.innerHTML=theMethod+` Request failed with status=[${
+ theStatus}] and message=[${theMessage}] `;
+ };
+
+ function showRequestData(theData) {
+ const itemRef=document.createElement('LI');
+ dataListRef.appendChild(itemRef);
+ const imgRef=document.createElement('IMG');
+ imgRef.setAttribute('src',theData.message);
+ itemRef.appendChild(imgRef);
+ dataTextRef.innerHTML='Concluded';
+ };
+};
+
+
+;
+
diff --git a/Week1/homework/js-exercises/project-hack-your-repo-I/hyf.png b/Week1/homework/js-exercises/project-hack-your-repo-I/hyf.png
new file mode 100644
index 000000000..76bc5a13b
Binary files /dev/null and b/Week1/homework/js-exercises/project-hack-your-repo-I/hyf.png differ
diff --git a/Week1/homework/js-exercises/project-hack-your-repo-I/index.html b/Week1/homework/js-exercises/project-hack-your-repo-I/index.html
new file mode 100644
index 000000000..9c8f80c1a
--- /dev/null
+++ b/Week1/homework/js-exercises/project-hack-your-repo-I/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ HYF-GITHUB
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week1/homework/js-exercises/project-hack-your-repo-I/index.js b/Week1/homework/js-exercises/project-hack-your-repo-I/index.js
new file mode 100644
index 000000000..45b769764
--- /dev/null
+++ b/Week1/homework/js-exercises/project-hack-your-repo-I/index.js
@@ -0,0 +1,94 @@
+
+'use strict';
+
+{
+ function fetchJSON(url, cb) {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.responseType = 'json';
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status <= 299) {
+ cb(null, xhr.response);
+ } else {
+ cb(new Error(`Network error: ${xhr.status} - ${xhr.statusText}`));
+ }
+ };
+ xhr.onerror = () => cb(new Error('Network request failed'));
+ xhr.send();
+ }
+
+ function createAndAppend(name, parent, options = {}) {
+ const elem = document.createElement(name);
+ parent.appendChild(elem);
+ Object.entries(options).forEach(([key, value]) => {
+ if (key === 'text') {
+ elem.textContent = value;
+ } else {
+ elem.setAttribute(key, value);
+ }
+ });
+ return elem;
+ }
+
+ function renderRepoDetails(repo, ul) {
+ createAndAppend('li', ul, { text: repo.name });
+ }
+
+ function renderRepoData(repoData,ul) {
+ const extractDateTime=(dtString)=>dtString.substr(0,10)+' '+dtString.substr(11,8);
+ const rowHTML=(aLabel,aValue,aLink)=>{
+ const anStr='title="Click for transition to related page." target="_blank"';
+ aValue=((aValue==undefined)||(aValue==null)||(String(aValue).trim()===''))
+ ?'-':aValue;
+ aValue=((aLink==undefined)||(aLink==null)||(String(aLink).trim()===''))
+ ?aValue:`${aValue} `;
+ return `${aLabel} ${aValue} `
+ };
+ repoData.sort((argA,argB)=>argA.name.localeCompare(argB.name));
+ for (let i=0; i<10; i++) {
+ const symbolExpandCollapse=(isHidden)=>isHidden?'➕':'➖'; // or '⏬ ':'⏫ '
+ const listItemRef=createAndAppend('LI',ul);
+ const initialHidden=false;
+ const itemRef=createAndAppend('DIV',listItemRef,{id:'id'+'_data_item_'+i
+ ,class:'cls_hover_highlight',text:`(${
+ symbolExpandCollapse(initialHidden)}) `+repoData[i].name
+ ,title:'Click to expand / collapse the details'});
+ itemRef.onclick=function(e) {
+ const trgtRef=document.getElementById(this.id+'_detail');
+ trgtRef.hidden=!trgtRef.hidden;
+ this.textContent='('+symbolExpandCollapse(trgtRef.hidden)
+ +this.textContent.substr(2);
+ }
+ const detailRef=createAndAppend('DIV',listItemRef
+ ,{id:itemRef.id+'_detail',class:'cls_alt_color'});
+ detailRef.hidden=initialHidden;
+ const tableRef=createAndAppend('TABLE',detailRef);
+ tableRef.innerHTML= rowHTML('Name (and link):',repoData[i].name,repoData[i].html_url)
+ + rowHTML('Description:',repoData[i].description)
+ + rowHTML('Forks:',repoData[i].forks_count)
+ + rowHTML('Created:',extractDateTime(repoData[i].created_at))
+ + rowHTML('Updated:',extractDateTime(repoData[i].updated_at));
+ };
+ };
+
+ function main(url) {
+ const root=document.getElementById('root');
+ createAndAppend('DIV',root,{class:'cls_colored_header',text:'HYF Repositories'});
+ root.setAttribute('class','cls_unselectable');
+ fetchJSON(url, (err, repos) => {
+ if (err) {
+ createAndAppend('div',root,{text:err.message,class:'alert-error'});
+ return;
+ }
+ renderRepoData(repos,createAndAppend('UL',root));
+ });
+ }
+
+ const HYF_REPOS_URL =
+ 'https://api.github.com/orgs/HackYourFuture/repos?per_page=100';
+ window.onload = () => main(HYF_REPOS_URL);
+};
+
+
+;
+
diff --git a/Week1/homework/js-exercises/project-hack-your-repo-I/style.css b/Week1/homework/js-exercises/project-hack-your-repo-I/style.css
new file mode 100644
index 000000000..31425126a
--- /dev/null
+++ b/Week1/homework/js-exercises/project-hack-your-repo-I/style.css
@@ -0,0 +1,138 @@
+
+
+:root {
+ --normal_back_color: wheat;
+ --normal_text_color: black;
+ --attention_back_color: lightblue;
+ --alternate_back_color: #0a1a3e; // dark blue-black
+ --alternate_text_color: #efefef;
+ --interactive_back_color: silver;
+ --interactive_text_color: black;
+ --hover_back_color: orange;
+ --hover_text_color: black;
+ --focus_back_color: yellow;
+ --focus_text_color: black;
+ --alert_back_color: red;
+ --alert_text_color: gold;
+}
+
+
+* {
+ vertical-align: middle;
+ padding: 0;
+ margin: 0;
+ cursor: default;
+ box-sizing: border-box;
+}
+
+
+:focus {
+ background-color: var(--focus_back_color);
+ outline: none;
+}
+
+
+body {
+ background-color: var(--normal_back_color);
+ color: var(--alternate_back_color);
+ margin-top: 5px;
+ font-size: 1.5rem;
+ font-family: "Roboto", sans-serif;
+}
+body:before {
+ background: url("./hyf.png");
+ background-size: cover;
+ content: " ";
+ position: fixed;
+ z-index: -1;
+ width: 42vmin;
+ height: 42vmin;
+ top: 1rem;
+ right: 1rem;
+ opacity: 0.24;
+}
+
+
+div {
+ padding: 16px 20px;
+}
+
+
+li {
+ list-style-type: none;
+}
+
+
+td {
+ padding: 0px 12px 3px 0px;
+ vertical-align: top;
+}
+
+
+a {
+ background-color: var(--interactive_back_color);
+ padding: 0 12px;
+}
+a:hover {
+ background-color: var(--hover_back_color);
+ text-decoration: none;
+}
+
+
+.cls_colored_header {
+ background: linear-gradient(to right, var(--attention_back_color) , rgba(128,128,128,0) );
+ font-weight: bold;
+ font-size: 1.7rem;
+ font-style: italic;
+}
+
+
+.cls_alt_color {
+ background: linear-gradient(to right, var(--attention_back_color) , rgba(128,128,128,0) );
+}
+
+
+.cls_hover_highlight {
+ background: linear-gradient(to right, var(--interactive_back_color) , rgba(128,128,128,0) );
+ margin-top: 8px;
+ font-weight: bold;
+ z-index: 1;
+ position: relative;
+}
+.cls_hover_highlight::before {
+ background: linear-gradient(to right, var(--hover_back_color) , rgba(128,128,128,0.05) );
+ content: "";
+ position: absolute;
+ opacity: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: -1;
+ transition: opacity 203ms linear;
+}
+.cls_hover_highlight:hover::before {
+ opacity: 1;
+}
+
+
+.alert-error {
+ background: linear-gradient(to right, var(--alert_back_color) , rgba(128,128,128,0) );
+ color: var(--alert_text_color);
+ font-size: 2rem;
+ font-weight: bold;
+}
+
+
+.cls_unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+
+;
+
diff --git a/Week2/homework/README.md b/Week2/homework/README.md
new file mode 100644
index 000000000..7aadbe9c2
--- /dev/null
+++ b/Week2/homework/README.md
@@ -0,0 +1,77 @@
+
+
+About the Project (the web page) :
+====================================
+Extra effort was included to fortify the page from breaking due to bad / unexpected data.
+
+For this reason extra functionality was included and there exists an option to run the page in data emulation mode
+(which actually is the initial page mode),
+wherein the data is not requested through an API call, but is instead supplied by an internal function, that produces data with diverse errors, nonexistent, unexpected or invalid values.
+
+To that end, a special "options" section has been added to the top of the page, whence from the page behavior can be influenced.
+
+All the originally-required essential HTML elements are produced through javascript code.
+
+The only additions to the actual html file are those introduced for the new "options" section, and that only so they can retain their settings between page refreshes.
+
+
+About the "options" section :
+====================================
+The "options" section can be toggled to visible / hidden by tapping the cog icon on the upper right corner.
+
+If hidden, it will stay so until revealed by taping the same icon (or when you refresh the web page and option #4 is checked).
+
+It can be kept permanently hidden after page refresh, by un-checking option #4.
+
+By default the options are set to emulate API calls, and will do so while option #1 is checked.
+
+Options #1 , #2 and #3 will influence the next fetch operation.
+(meaning you could mix & match "emulated" and "live" data).
+An Exception is when the page gets refreshed, where there are 2 consecutive fetches, both of which are subjected to the specified options.
+
+Error production (option #2) is done by:
+1. in case of emulated data a sample error is thrown
+2. in case of real API call, the URL is slightly altered to produce an error response
+
+
+Empty datasets (option #3) are always emulated, regardles of option #1
+
+In case when options #3 and #4 are both checked, then option #3 has precedence, meaning an error result will be produced before an empty dataset.
+
+
+About the "emulated" data :
+====================================
+The emulated "repositories" result consists of a special data collection with a multitude of nonexistent, erroneous, null, undefined, empty or missing values, test values, missing names, same names with upper/lower case differences, etc...
+
+It is supposed to represent possible data problems a page may encounter, and has to fortify against in order to work correctly.
+
+The subsequent "contributors" results are semi-random, meaning that names are completely random, but images and links are real, tho inserted into random objects, and also the resulting contributor list length is randomized.
+
+
+About the "live" API data :
+====================================
+Some subsequent (contributor list) fetches fail because of bad links.
+Located repos with links that fail are:
+1. english-booster
+2. hyfer-infra
+
+Those 2 hold broken values in their "contributors_url".
+
+Evoking the addresses of those urls in the browser yields empty pages.
+
+Also the repo "DataStructures" has an empty contrib list result.
+
+
+About the data in general :
+====================================
+In case of subsequent fetch errors (those for contributors) I have opted not to display an error but instead to just log the error and the link in the console, and then let the page behave as if an empty result was received.
+
+In order to avoid web traffic, and possible API request refusal due to frequency, "contributors" fetching is done once for each repo, and that result is stored and used until page refresh.
+
+That means that, once a result has been received, whether it is an actual list, an empty dataset or an error which results in an empty dataset, that will be the result displayed each time the user selects that repo from the repo selection element.
+
+
+Bunus objective: undesired API request prevention
+====================================
+Additional effort was put into the prevention of consecutive API requests when the contents of the repo SELECT item are scrolled rapidly through by the user. To that end, once a repo-change has initiated an API request, all subsequent repo-change-handling is halted until that API request has concluded.
+
diff --git a/Week2/homework/project-hack-your-repo-II/hyf.png b/Week2/homework/project-hack-your-repo-II/hyf.png
new file mode 100644
index 000000000..76bc5a13b
Binary files /dev/null and b/Week2/homework/project-hack-your-repo-II/hyf.png differ
diff --git a/Week2/homework/project-hack-your-repo-II/index.html b/Week2/homework/project-hack-your-repo-II/index.html
new file mode 100644
index 000000000..845356a90
--- /dev/null
+++ b/Week2/homework/project-hack-your-repo-II/index.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HYF-GITHUB
+
+
+
+
+
+
+
+
+
+ Due to GitHub’s strict rules on API request frequency,
+ you may experience a "forbidden" response.
+
+
+ In that case ( or for testing, or even for fun )
+ you can use the following options to "emulate"
+ API data fetching with sample / random Data.
+
+
+ Note: set options persist even after page refresh,
+ thus enabling them to influence the initial auto-fetch
+ that creates the Repository List.
+
+
+
+
+
+
+
+
+
+
diff --git a/Week2/homework/project-hack-your-repo-II/index.js b/Week2/homework/project-hack-your-repo-II/index.js
new file mode 100644
index 000000000..1b9c17ae9
--- /dev/null
+++ b/Week2/homework/project-hack-your-repo-II/index.js
@@ -0,0 +1,367 @@
+
+'use strict';
+
+{
+ const root=document.getElementById('root');
+ // root.setAttribute('class','cls_unselectable');
+ //
+ const hdrRef=addToDOM('DIV',root,{class:'cls_colored_header'});
+ const msgRef=addToDOM('SPAN',addToDOM('DIV',hdrRef
+ ,{class:'cls_responsive_child cls_padded_area'})
+ ,{text:'API data request in progress'});
+ const dtlRef=addToDOM('DIV',root,{class:'cls_responsive_parent'});
+ const contribSymbolKey=Symbol('DOT.NOT: contributor list key');
+ //
+ const optEmulation =document.getElementById('id_opt_emulation');
+ const optForceError=document.getElementById('id_opt_force_error');
+ const optForceEmpty=document.getElementById('id_opt_force_empty');
+ const optKeepVisible=document.getElementById('id_opt_keep_visible');
+ const optAreaRef=document.getElementById('id_options_area');
+ optAreaRef.hidden=!optKeepVisible.checked;
+ const optButton=document.getElementById('id_options_image');
+ const optBtnTitle=()=>{return `Tap to ${optAreaRef.hidden?'show':'hide'} Options`};
+ optButton.title=optBtnTitle();
+ optButton.onclick=()=>{
+ optAreaRef.hidden=!optAreaRef.hidden;
+ optButton.title=optBtnTitle();
+ };
+ let repoData=[],selectRef;
+ const rootURL='https://api.github.com/orgs/HackYourFuture/repos?per_page=100';
+ //
+ window.onload=()=>{
+ fetchWrapper('repositories',(theResponse)=>{
+ repoData=clone_JSON_object(theResponse) // theResponse
+ .map(lmnt=>{return {
+ name :getSecureValue(lmnt.name,'- not named -'),
+ description :getSecureValue(lmnt.description,'-'),
+ forks_count :getSecureValue(lmnt.forks_count,'0'),
+ created_at :getSecureValue(lmnt.created_at,'-'),
+ updated_at :getSecureValue(lmnt.updated_at,'-'),
+ html_url :getSecureValue(lmnt.html_url),
+ contributors_url:getSecureValue(lmnt.contributors_url),
+ }})
+ .sort((argA,argB)=>argA.name.localeCompare(argB.name));
+ createRepoSelectionList();
+ },(anError)=>{
+ msgRef.textContent='API data request error.';
+ addToDOM('div',root,{text:anError,class:'cls_error_alert'});
+ });
+ };
+
+ function createRepoSelectionList () {
+ if (repoData.length<1) { msgRef.textContent='No repositories available!';
+ } else {
+ msgRef.textContent='HYF Repositories';
+ selectRef=addToDOM('SELECT',addToDOM('DIV',hdrRef
+ ,{class:'cls_responsive_child cls_padded_area'}));
+ selectRef.innerHTML=repoData.reduce((tot,cur,idx)=>
+ tot+`${cur.name} `,'');
+ selectRef.onchange=createRepoDetails;
+ createRepoDetails();
+ }
+ };
+
+ function createRepoDetails() {
+ const had_onchange=(selectRef.onchange!==null);
+ if (had_onchange) {selectRef.onchange=null};
+ const current_repoID=Number(selectRef.value);
+ removeChildrenFromDOM(dtlRef);
+ const repoRef=addToDOM('TABLE',addToDOM('DIV',addToDOM('DIV',dtlRef
+ ,{class:'cls_responsive_child'}),{class:'cls_alt_color cls_padded_area'}));
+ const theRepo=repoData[current_repoID];
+ repoRef.innerHTML= tableRowHTML('Repository:',theRepo.name,theRepo.html_url)
+ + tableRowHTML('Description:',theRepo.description)
+ + tableRowHTML('Forks:',theRepo.forks_count)
+ + tableRowHTML('Created:',extractDateTime(theRepo.created_at))
+ + tableRowHTML('Updated:',extractDateTime(theRepo.updated_at));
+ const listRef=addToDOM('DIV',dtlRef
+ ,{class:'cls_responsive_child cls_alt_color cls_padded_area'});
+ const titleRef=addToDOM('P',listRef,{text:'API data request in progress'});
+ if (theRepo[contribSymbolKey]==undefined) {
+ fetchWrapper(theRepo.contributors_url,(theResponse)=>{
+ theRepo[contribSymbolKey]=clone_JSON_object(theResponse)
+ .map(lmnt=>{return {
+ html_url :getSecureValue(lmnt.html_url),
+ avatar_url :getSecureValue(lmnt.avatar_url),
+ login :getSecureValue(lmnt.login,'-'),
+ contributions:getSecureValue(lmnt.contributions,'0')
+ }});
+ createContribList(theRepo[contribSymbolKey]);
+ },(anError)=>{
+ /*
+ // ignore any possible errors
+ // instead act as if contrib request returned empty list
+ //
+ // because the actual repo list contains repos
+ // that return an error when asked for contributors
+ //
+ // repos with error in contributor links
+ // english-booster
+ // hyfer-infra
+ //
+ // repo without contributors
+ // DataStructures
+ //
+ //
+ // BUT: make sure to console.log the link and error
+ //
+ titleRef.textContent='API data request error.';
+ addToDOM('DIV',listRef,{text:anError})
+ finalizeRepoDetails();
+ */
+ console.log('API request error for url',theRepo.contributors_url);
+ console.log('Error message',anError);
+ theRepo[contribSymbolKey]=[];
+ createContribList(theRepo[contribSymbolKey]);
+ });
+ } else {
+ createContribList(theRepo[contribSymbolKey]);
+ };
+ function createContribList(theData) {
+ if (theData.length<1) {titleRef.textContent='No contributors available!'}
+ else {
+ titleRef.innerHTML='Contributions ';
+ const contRef=addToDOM('TABLE',addToDOM('DIV',listRef)
+ ,{class:'cls_span_full_parent_width'});
+ let theHTML='';
+ theData.forEach(lmnt=>{
+ const theIMG=` `;
+ theHTML+=`${tableCellHTML(theIMG)}${
+ tableCellHTML(lmnt.login,lmnt.html_url)}${
+ tableCellHTML(lmnt.contributions)} `;
+ });
+ contRef.innerHTML=theHTML;
+ };
+ finalizeRepoDetails();
+ };
+ function finalizeRepoDetails() {
+ if (current_repoID!=selectRef.value) {createRepoDetails()};
+ if (had_onchange) {selectRef.onchange=createRepoDetails};
+ };
+ function tableRowHTML(aLabel,aValue,aLink) {
+ return `${tableCellHTML(aLabel,null,false,true)}${
+ tableCellHTML(aValue,aLink,true)} `;
+ };
+ function tableCellHTML(theValue,theLink=null,makeBold=false,makeItalic=false) {
+ let result=getValidAnchor(theValue,theLink);
+ if (makeBold) {result=`${result} `};
+ if (makeItalic) {result=`${result} `};
+ return `${result} `;
+ };
+ function getValidAnchor(aText,aLink) {
+ const trg='title="Tap for transition to related page." target="_blank"';
+ return isUndefinedNullEmpty(aLink)?aText:`${aText} `;
+ };
+ function extractDateTime(dtString){
+ return dtString.substr(0,10)+' '+dtString.substr(11,8);
+ };
+ };
+
+ function isUndefinedNullEmpty(theData) {
+ return (theData==undefined)||(theData==null)||(String(theData).trim()==='');
+ };
+
+ function getSecureValue(theData,defValue='') {
+ return isUndefinedNullEmpty(theData)?defValue:theData;
+ };
+
+ function clone_JSON_object(theObject) {return JSON.parse(JSON.stringify(theObject))};
+
+ function addToDOM(theTag,theParent,theOptions={}) {
+ const lmnt=document.createElement(theTag);
+ Object.entries(theOptions).forEach(([aKey,aValue])=>{
+ if (aKey==='text') {lmnt.textContent=aValue}
+ else {lmnt.setAttribute(aKey,aValue)};
+ });
+ theParent.appendChild(lmnt);
+ return lmnt;
+ };
+
+ function removeChildrenFromDOM (theLmnt) {
+ while (theLmnt.hasChildNodes()) {
+ removeChildrenFromDOM(theLmnt.lastChild);
+ theLmnt.removeChild(theLmnt.lastChild);
+ };
+ };
+
+ function fetchWrapper (theResource,onData,onError) {
+ if (optEmulation.checked) {
+ if (theResource==='repositories') {
+ fetchTestData(optForceError.checked,optForceEmpty.checked
+ ,theResource,onData,onError);
+ } else {
+ fetchTestData(optForceError.checked,optForceEmpty.checked
+ ,'contributors',onData,onError);
+ };
+ } else {
+ if (theResource==='repositories') {
+ if (optForceError.checked) {
+ const targetURL='https://api.github.com/orgs/HackYourFuture/Prepos?per_page=1';
+ fetchData(targetURL,onData,onError);
+ } else if (optForceEmpty.checked) {
+ fetchTestData(false,true,theResource,onData,onError);
+ } else {
+ fetchData(rootURL,onData,onError);
+ };
+ } else {
+ if (optForceError.checked) {
+ const targetURL='https://api.github.com/orgs/HackYourFuture/Prepos?per_page=1';
+ fetchData(targetURL,onData,onError);
+ } else if (optForceEmpty.checked) {
+ fetchTestData(false,true,theResource,onData,onError);
+ } else {
+ fetchData(theResource,onData,onError);
+ };
+ };
+ };
+ };
+
+ function fetchData (dataURL,dataCallback,errorCallback) {
+ fetch(dataURL)
+ .then((theResponse)=>{
+ if (!theResponse.ok) {
+ const errPrefix=`Request Error: status=${theResponse.status} msg=`;
+ throw errPrefix+theResponse.statusText;
+ };
+ return theResponse.json();
+ })
+ .then((theData)=>{dataCallback(theData)})
+ .catch((anError)=>{errorCallback(anError)});
+ };
+
+ function fetchTestData (produceError,produceEmpty,dataURL,dataCallback,errorCallback) {
+ function getRandomNumberBetween (minValue,maxValue){return (Math.floor(Math.pow(10,14)*
+ Math.random()*Math.random())%(maxValue-minValue+1))+minValue};
+ function delayedCallback(argDelay,argCB,argData) {setTimeout(()=>{argCB(argData)},argDelay)};
+ const repositories = [
+ {name:'One Thing', description:'description of 1st One Thing', forks_count:7,
+ created_at:'2005-04-24T07:20:48Z', updated_at:'2019-01-11T18:21:37Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'one Thing', description:'description of 2nd one Thing', forks_count:3,
+ created_at:'2006-03-23T07:19:38Z', updated_at:'2017-09-05T15:52:37Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'one thing', description:'description of 3rd one thing', forks_count:1,
+ created_at:'2008-01-21T07:17:18Z', updated_at:'2013-11-15T11:02:37Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'One thing', description:'description of 4th One thing', forks_count:6,
+ created_at:'2007-02-22T07:18:28Z', updated_at:'2015-07-25T13:48:37Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'another thing', description:'another thing (missing link)', forks_count:4,
+ created_at:'2017-04-03T17:26:16Z', updated_at:'2020.01.11T05:46:02Z',
+ },
+ {name:'that’s the thing', description:'description of that’s the thing', forks_count:1,
+ created_at:'2016-08-07T19:17:31Z', updated_at:'2017-09-14T10:34:47Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'The others', description:'description of The others', forks_count:7,
+ created_at:'2014-12-25T05:57:35Z', updated_at:'2020-02-13T21:31:47Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'JustLookAtThisGargantuanRepositoryNameWithEmptyDescription', description:' ', forks_count:9,
+ created_at:'2011-10-04T21:49:54Z', updated_at:'2013-05-10T16:01:23Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'that thing again', description:'description of that thing again', forks_count:3,
+ created_at:'2017-03-14T02:20:29Z', updated_at:'2020-03-01T06:34:36Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'just some more', description:'just some more (link is null)', forks_count:5,
+ created_at:'2015-10-12T00:14:52Z', updated_at:'2018-10-20T10:31:57Z',
+ html_url:null},
+ {name:'everything', description:'everything (empty link)', forks_count:4,
+ created_at:'2016-03-30T01:25:17Z', updated_at:'2019-04-30T15:42:34Z',
+ html_url:' '},
+ // {name:'everything else', description:'description of everything else', forks_count:4,
+ {name:'everything else (missing description and dates)', forks_count:4,
+ //created_at:'2017-09-23T21:15:20Z', updated_at:'2020-01-11T15:02:51Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ //{name:'another life', description:'description of another life', forks_count:3,
+ {name:'another life (null description & dates)', description:null, forks_count:3,
+ //created_at:'2015-07-17T22:41:40Z', updated_at:'2017-05-28T19:04:24Z',
+ created_at:null, updated_at:'',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'one more please', description:'description of one more please', forks_count:2,
+ created_at:'2014-02-28T12:54:51Z', updated_at:'2020-02-15T19:51:54Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'Another brand', description:'description of another brand', forks_count:8,
+ created_at:'2013-06-21T04:36:50Z', updated_at:'2018-12-16T07:11:13Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'useless things', description:'description of useless things', forks_count:6,
+ created_at:'2011-01-31T09:23:38Z', updated_at:'2017-11-28T10:34:16Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'good to know', description:'description of good to know', forks_count:0,
+ created_at:'2019-02-23T06:32:43Z', updated_at:'2020-01-05T02:09:21Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'turn around', description:'description of turn around', forks_count:2,
+ created_at:'2013-11-19T17:34:22Z', updated_at:'2016-07-30T16:48:14Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'turn some more', description:'description of turn some more', forks_count:0,
+ created_at:'2014-05-09T15:29:00Z', updated_at:'2019-09-04T12:02:45Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:' ', description:'description of (empty name)', forks_count:' ',
+ created_at:'2015.06.04T17:50:07Z', updated_at:'2020-02-01T01:29:33Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ { description:'description of (missing name)',
+ created_at:'2015.06.04T17:50:07Z', updated_at:'2020-02-01T01:29:33Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:null, description:'description of (null name)', forks_count:null,
+ created_at:'2015.06.04T17:50:07Z', updated_at:'2020-02-01T01:29:33Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'abusing power', description:'everything you ever wanted to know but where afraid to ask'
+ , forks_count:12,
+ created_at:'2011-11-22T05:05:18Z', updated_at:'2020-03-01T06:20:51Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'what a name', description:'description of what a name', forks_count:6,
+ created_at:'2017-12-10T08:37:15Z', updated_at:'2017-12-30T17:32:15Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ {name:'what goes there', description:'description of what goes there', forks_count:0,
+ created_at:'2012-02-14T15:01:08Z', updated_at:'2020-02-25T01:41:41Z',
+ html_url:'https://github.com/SocialHackersCodeSchool'},
+ ];
+ const contrib_img_url = [
+ 'https://avatars3.githubusercontent.com/u/8708858?v=4',
+ 'https://avatars3.githubusercontent.com/u/969026?v=4',
+ 'https://avatars3.githubusercontent.com/u/33282467?v=4',
+ 'https://avatars0.githubusercontent.com/u/651290?v=4',
+ 'https://avatars2.githubusercontent.com/u/17527017?v=4',
+ 'https://avatars0.githubusercontent.com/u/13186712?v=4',
+ 'https://avatars0.githubusercontent.com/u/15912395?v=4',
+ 'https://avatars0.githubusercontent.com/u/1047135?v=4',
+ 'https://avatars3.githubusercontent.com/u/45293?v=4',
+ 'https://avatars3.githubusercontent.com/u/22626039?v=4',
+ 'https://avatars3.githubusercontent.com/u/658656?v=4',
+ 'https://avatars3.githubusercontent.com/u/988347?v=4',
+ 'https://avatars3.githubusercontent.com/u/33317975?v=4',
+ 'https://avatars0.githubusercontent.com/u/25272822?v=4',
+ 'https://avatars0.githubusercontent.com/u/6762913?v=4',
+ 'https://avatars2.githubusercontent.com/u/33654086?v=4',
+ 'https://avatars1.githubusercontent.com/u/10981911?v=4',
+ 'https://avatars0.githubusercontent.com/u/43432?v=4',
+ 'https://avatars0.githubusercontent.com/u/32513012?v=4',
+ 'https://avatars2.githubusercontent.com/u/7113309?v=4',
+ 'https://avatars0.githubusercontent.com/u/2788771?v=4',
+ ];
+ if (produceError) {
+ delayedCallback(567,errorCallback,'Error: status=707 msg="Example error".');
+ } else if (produceEmpty) {
+ delayedCallback(678,dataCallback,[]);
+ } else {
+ let theData=[];
+ if (dataURL=='repositories') {theData=clone_JSON_object(repositories)}
+ else if (dataURL=='contributors') {
+ const resultLength=getRandomNumberBetween(0,20);
+ for (let i=0; i<=resultLength; i++) {
+ const theLink=contrib_img_url.splice(getRandomNumberBetween(0,contrib_img_url.length-1),1);
+ theData.push({
+ html_url : theLink,
+ avatar_url : theLink,
+ login : Math.random().toString(36).substring(2),
+ contributions : getRandomNumberBetween(1,99)
+ });
+ }
+ };
+ delayedCallback(789,dataCallback,theData);
+ };
+ };
+
+};
+
+
+;
+
diff --git a/Week2/homework/project-hack-your-repo-II/options.png b/Week2/homework/project-hack-your-repo-II/options.png
new file mode 100644
index 000000000..3703ca053
Binary files /dev/null and b/Week2/homework/project-hack-your-repo-II/options.png differ
diff --git a/Week2/homework/project-hack-your-repo-II/style.css b/Week2/homework/project-hack-your-repo-II/style.css
new file mode 100644
index 000000000..ff1cd88f0
--- /dev/null
+++ b/Week2/homework/project-hack-your-repo-II/style.css
@@ -0,0 +1,204 @@
+
+:root {
+ /* unused interesting colors */
+ /* yellow: #FFFF00 - rgb(255, 255, 0) */
+ /* orange: #FFA500 - rgb(255, 165, 0) */
+ /* silver: #C0C0C0 - rgb(192, 192, 192) */
+ /* dark-blue #0A1A3E - rgb(10, 26, 62) */
+ /* silver-white #EFEFEF - rgb(239, 239, 239) */
+
+ /* wheat: #F5DEB3 - rgb(245, 222, 179) */
+ --color_page_normal_bg: #F5DEB3;
+ /* black: #000000 - rgb(0, 0, 0) */
+ --color_page_normal_text: #000000;
+
+ /* mid-blue #6EBEFE - rgb(110, 190, 254) */
+ --color_attention_bg_grad_begin: #6EBEFE;
+ /* ^ with 70% transparency */
+ --color_attention_bg_grad_end: rgba(110,190,253,0.3);
+
+ /* lightblue: #ADD8E6 - rgb(173, 216, 230) */
+ --color_list_bg_grad_begin: #ADD8E6;
+ /* ^ with 90% transparency */
+ --color_list_bg_grad_end: rgba(173,216,230,0.1);
+
+ /* black: #000000 - rgb(0, 0, 0) */
+ --color_focus_outline: #000000;
+
+ /* light-silver #E5E5E5 - rgb(229, 229, 229) */
+ --color_interactive_bg_grad_begin: #E5E5E5;
+ /* light-silver #E0E0E0 - rgb(224, 224, 224) */
+ --color_interactive_bg_grad_end: #E0E0E0;
+
+ /* red: #FF0000 - rgb(255, 0, 0) */
+ --color_alert_bg_grad_begin: #FF0000;
+ /* gold: #FFD700 - rgb(255, 215, 0) */
+ --color_alert_text: #FFD700;
+
+ /* WhiteSmoke: #F5F5F5 - rgb(245,245,245) */
+ --color_hover_bg_grad_begin: #F5F5F5;
+ /* silver-white #F0F0F0 - rgb(240,240,240) */
+ --color_hover_bg_grad_end: #F0F0F0;
+}
+
+/* resets */
+body, div, table, tr, td, ul, li, a, p, img, span {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ cursor: default;
+ vertical-align: middle;
+}
+
+
+body {
+ background-color: var(--color_page_normal_bg);
+ color: var(--color_page_normal_text);
+ margin: 12px 12px 0;
+ font-size: 1.5rem;
+ font-family: "Roboto", sans-serif;
+}
+body:before {
+ background: url("./hyf.png");
+ background-size: cover;
+ content: " ";
+ position: fixed;
+ z-index: -1;
+ width: 42vmin;
+ height: 42vmin;
+ top: 1rem;
+ right: 1rem;
+ opacity: 0.24;
+}
+
+
+li {
+ list-style-type: none;
+}
+
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+
+td {
+ padding-top: 6px;
+ padding-bottom: 2px;
+}
+td:nth-child(1) {
+ padding-right: 8px;
+ vertical-align: top;
+}
+td:nth-child(2) {
+ overflow-wrap: anywhere;
+ padding-left: 8px;
+}
+td:nth-child(3) {
+ padding-left: 16px;
+ text-align: right;
+}
+
+
+select {
+ font-size: 1.4rem;
+ width: 99%;
+}
+
+
+a {
+ background: linear-gradient(to bottom, var(--color_interactive_bg_grad_begin) , var(--color_interactive_bg_grad_end) );
+}
+a:hover {
+ background: linear-gradient(to bottom, var(--color_hover_bg_grad_begin) , var(--color_hover_bg_grad_end) );
+}
+
+
+*:focus {
+ outline: 4px solid var(--color_focus_outline);
+}
+
+
+.cls_colored_header {
+ background: linear-gradient(to right, var(--color_attention_bg_grad_begin) , var(--color_attention_bg_grad_end) );
+ font-weight: bold;
+ font-size: 1.7rem;
+ font-style: italic;
+ padding: 10px 0 8px;
+}
+
+
+.cls_responsive_parent {
+ display: grid;
+ grid-gap: 0;
+ grid-template-columns:repeat(auto-fit,minmax(24rem,1fr));
+}
+
+
+.cls_responsive_child {
+ max-width: 90vw;
+ display: inline-block;
+}
+
+
+.cls_padded_area {
+ padding-left: 12px;
+ padding-right: 12px;
+}
+
+
+.cls_alt_color {
+ background: linear-gradient(to right, var(--color_list_bg_grad_begin) , var(--color_list_bg_grad_end) );
+ margin-top: 12px;
+ padding-top: 8px;
+ padding-bottom: 6px;
+}
+
+
+#id_options_area {
+ background-color: var(--color_list_bg_grad_begin);
+ font-size: 1.4rem;
+ margin-bottom: 12px;
+ padding: 10px 12px 8px 14px;
+}
+
+
+#id_options_image {
+ float: right;
+ margin-left: 6px;
+ margin-bottom: 6px;
+}
+
+
+.cls_options_element {
+ margin-bottom: 8px;
+}
+
+
+.cls_span_full_parent_width {
+ width: 99%;
+}
+
+
+.cls_error_alert {
+ background: linear-gradient(to right, var(--color_alert_bg_grad_begin) , rgba(128,128,128,0) );
+ color: var(--color_alert_text);
+ font-size: 2rem;
+ font-weight: bold;
+ padding: 8px 12px 6px;
+}
+
+
+.cls_unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+
+;
+
diff --git a/Week3/homework/README.md b/Week3/homework/README.md
new file mode 100644
index 000000000..d04a26cc4
--- /dev/null
+++ b/Week3/homework/README.md
@@ -0,0 +1,4 @@
+
+Homework folder for JS-3 - week 3
+=====================
+
diff --git a/Week3/homework/project-hack-your-repo-III/App.js b/Week3/homework/project-hack-your-repo-III/App.js
new file mode 100644
index 000000000..8788f8b85
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/App.js
@@ -0,0 +1,56 @@
+'use strict';
+
+{
+ const accounts = {
+ hyf: {
+ name: 'HackYourFuture',
+ type: 'org',
+ },
+ microsoft: {
+ name: 'Microsoft',
+ type: 'org',
+ },
+ jim: {
+ name: 'remarcmij',
+ type: 'user',
+ },
+ };
+
+ const { Model, HeaderView, RepoView, ContributorsView, ErrorView } = window;
+ const { createAndAppend } = window.Util;
+
+ class App {
+ constructor(account) {
+ const containers = App.renderContainers();
+
+ const model = new Model(account);
+ const fetchData = model.fetchData.bind(model);
+
+ model.subscribe(new HeaderView(account, containers.header, fetchData));
+ model.subscribe(new RepoView(containers.repo));
+ model.subscribe(new ContributorsView(containers.contributors));
+ model.subscribe(new ErrorView(containers.error));
+
+ fetchData();
+ }
+
+ static renderContainers() {
+ const root = document.getElementById('root');
+ const header = createAndAppend('header', root, { class: 'header' });
+ const error = createAndAppend('div', root);
+ const main = createAndAppend('main', root, {
+ class: 'main-container',
+ });
+ const repo = createAndAppend('section', main, {
+ class: 'repo-container whiteframe',
+ });
+ const contributors = createAndAppend('section', main, {
+ class: 'contributors-container whiteframe',
+ });
+ return { header, error, main, repo, contributors };
+ }
+ }
+
+ const ACCOUNT_KEY = 'hyf';
+ window.onload = () => new App(accounts[ACCOUNT_KEY]);
+}
diff --git a/Week3/homework/project-hack-your-repo-III/ContributorsView.js b/Week3/homework/project-hack-your-repo-III/ContributorsView.js
new file mode 100644
index 000000000..86a7f6f41
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/ContributorsView.js
@@ -0,0 +1,48 @@
+'use strict';
+
+{
+ // DOT.NOT:JS-3:w-3:project: include additional utility functions
+ const { createAndAppend , createTableRowHTML ,
+ getValidValue , createTableCellObject } = window.Util;
+
+ class ContributorsView {
+ constructor(container) {
+ this.container = container;
+ // DOT.NOT:JS-3:w-3:project: additional rendering / styling elements
+ const wrapper=createAndAppend('DIV',this.container
+ ,{id:'id_wrap_contributors'});
+ this.titleRef=createAndAppend('P',wrapper,{id:'id_title_contributors'});
+ this.dataList=createAndAppend('TABLE',wrapper
+ ,{id:'id_table_contributors'});
+ }
+
+ update(state) {
+ if (!state.error) {
+ this.render(state.contributors);
+ }
+ }
+
+ /**
+ * Renders the list of contributors
+ * @param {Object[]} contributors An array of contributor objects
+ */
+ render(contributors) {
+ // DOT.NOT:JS-3:w-3:project: implement contributors rendering
+ const cntrbTitle=(cnt)=>`${cnt<1?'No':cnt} Contributor${cnt===1?'':'s'}`;
+ this.titleRef.innerHTML=cntrbTitle((contributors.length||0));
+ let theHTML='';
+ if ((contributors) && (contributors.length>0)) {
+ theHTML=contributors.reduce((tot,cur)=>tot+createTableRowHTML([
+ createTableCellObject(` `),
+ createTableCellObject(getValidValue(cur.login),
+ getValidValue(cur.html_url),true),
+ createTableCellObject(getValidValue(cur.contributions,0)),]),'');
+ };
+ this.dataList.innerHTML=theHTML;
+ };
+
+ }
+
+ window.ContributorsView = ContributorsView;
+}
diff --git a/Week3/homework/project-hack-your-repo-III/ErrorView.js b/Week3/homework/project-hack-your-repo-III/ErrorView.js
new file mode 100644
index 000000000..67ad53087
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/ErrorView.js
@@ -0,0 +1,31 @@
+'use strict';
+
+{
+ const { createAndAppend } = window.Util;
+
+ class ErrorView {
+ constructor(container) {
+ this.container = container;
+ }
+
+ update(state) {
+ this.render(state.error);
+ }
+
+ /**
+ * Renders an error for the 'error' message type.
+ * @param {Error} error An Error object
+ */
+ render(error) {
+ this.container.innerHTML = '';
+ if (error) {
+ createAndAppend('div', this.container, {
+ text: error.message,
+ class: 'alert alert-error',
+ });
+ }
+ }
+ }
+
+ window.ErrorView = ErrorView;
+}
diff --git a/Week3/homework/project-hack-your-repo-III/HeaderView.js b/Week3/homework/project-hack-your-repo-III/HeaderView.js
new file mode 100644
index 000000000..11f9c8971
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/HeaderView.js
@@ -0,0 +1,46 @@
+'use strict';
+
+{
+ const { createAndAppend } = window.Util;
+
+ class HeaderView {
+ constructor(account, header, fetchData) {
+ this.account = account;
+ this.header = header;
+ this.fetchData = fetchData;
+ this.select = null;
+ }
+
+ update(state) {
+ if (!this.select && !state.error) {
+ this.render(state.repos);
+ }
+ }
+
+ /**
+ * Renders the data for the 'select' message type. Create a element
+ * and its children.
+ * @param {Object[]} repos An array of repository objects.
+ */
+ render(repos) {
+ createAndAppend('div', this.header, { text: this.account.name });
+ this.select = createAndAppend('select', this.header, {
+ class: 'repo-select',
+ autofocus: 'autofocus',
+ });
+
+ repos.forEach(repo =>
+ createAndAppend('option', this.select, {
+ text: repo.name,
+ value: repo.id,
+ }),
+ );
+
+ this.select.addEventListener('change', () =>
+ this.fetchData(this.select.value),
+ );
+ }
+ }
+
+ window.HeaderView = HeaderView;
+}
diff --git a/Week3/homework/project-hack-your-repo-III/Model.js b/Week3/homework/project-hack-your-repo-III/Model.js
new file mode 100644
index 000000000..302983965
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/Model.js
@@ -0,0 +1,68 @@
+'use strict';
+
+{
+ const { Observable } = window;
+
+ const makeUrl = ({ name, type }) =>
+ `https://api.github.com/${type}s/${name}/repos?per_page=100`;
+
+ class Model extends Observable {
+ constructor(account) {
+ super();
+ this.account = account;
+ this.state = {
+ repos: [],
+ selectedRepo: null,
+ contributors: [],
+ error: null,
+ };
+ }
+
+ async fetchData(id) {
+ const repoId = parseInt(id, 10);
+ this.state.error = null;
+ try {
+ if (this.state.repos.length === 0) {
+ const repos = await Model.fetchJSON(makeUrl(this.account));
+ this.state.repos = repos.sort((a, b) => a.name.localeCompare(b.name));
+ }
+ const index = id
+ ? this.state.repos.findIndex(repo => repo.id === repoId)
+ : 0;
+ this.state.selectedRepo = this.state.repos[index];
+ this.state.contributors = await Model.fetchJSON(
+ this.state.selectedRepo.contributors_url,
+ );
+ } catch (err) {
+ this.state.error = err;
+ }
+ this.notify(this.state);
+ }
+
+ // DOT.NOT:JS-3:w-3:project: replace fetch with axios & async/await
+ //
+ // following block is original fetchJSON function
+ // now commented out and replaced by block bellow
+ //
+ // static fetchJSON(url) {
+ // return fetch(url).then(res => {
+ // if (!res.ok) {
+ // return new Error(`HTTP ${res.status} - ${res.statusText}`);
+ // }
+ // return res.status === 200 ? res.json() : null;
+ // });
+ // }
+ //
+ static async fetchJSON(url) {
+ const res=await axios.get(url);
+ if (res.status !== 200) {
+ return new Error(`HTTP ${res.status} - ${res.statusText}`);
+ };
+ return res.data;
+ };
+
+ }
+
+ window.Model = Model;
+}
+
diff --git a/Week3/homework/project-hack-your-repo-III/Observable.js b/Week3/homework/project-hack-your-repo-III/Observable.js
new file mode 100644
index 000000000..3eb5b2530
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/Observable.js
@@ -0,0 +1,20 @@
+'use strict';
+
+{
+ class Observable {
+ constructor() {
+ this.observers = new Set();
+ }
+
+ subscribe(observer = {}) {
+ this.observers.add(observer);
+ return () => this.observers.delete(observer);
+ }
+
+ notify(data) {
+ this.observers.forEach(observer => observer.update(data));
+ }
+ }
+
+ window.Observable = Observable;
+}
diff --git a/Week3/homework/project-hack-your-repo-III/README.md b/Week3/homework/project-hack-your-repo-III/README.md
new file mode 100644
index 000000000..c3114b13c
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/README.md
@@ -0,0 +1,37 @@
+
+PROJECT: Hack Your Repo III
+=====================
+
+
+PART 1: Modify API data request technique: replace fetch.then().catch() with Axios-async/await
+=====================
+Applied changes are :
+1. index.html : Included script for axios library
+2. Model.js : Modified function fetchJSON() to implement requirements:
+converted to async method, replaced fetch API cal with axios and included await for results
+
+
+PART 2: Moving to the OOP version of the homework
+=====================
+1. Util.js : Added 3 utility functions for data rendering
+2. RepoView.js : Implemented repository data rendering
+3. ContributorsView.js : Implemented contributors data rendering
+4. style.css : Implemented page styles
+
+
+BONUS: A case study on caveats
+=====================
+Multiple consecutive data fetches & renders
+
+By design, if the user focuses on the repository selection field without opening the list (with TAB for example) or after a list item was selected, and then presses and holds UP/DOWN arrow buttons, the code will ussue a number of data fetches & renders, equal to the traversed repos, resulting in an equal number of quick consecutive data redraws.
+
+As a proof-of-caveat, this behavior can be clearly observed by opening the original unaltered page, found at
+https://github.com/SocialHackersClass10/JavaScript3/tree/master/homework-classes
+which does a console log for every fetch and thus demonstrates the effect.
+
+A modification to the fetchData() evocation, whence the fetch & render cycle triggers from, has to be implemented to prevent such ( ? undesired ? ) behavior. Said modification would only re-evoke fetchData() once the currently ongoing fetch & render cycle has completed.
+
+That modification would be implemented in HeaderView.js and should involve the change-handler function of the eventListener.
+
+A similar nehavior prevention modification has already been implemented by me as proof-of-concept in the project of week 2.
+
diff --git a/Week3/homework/project-hack-your-repo-III/RepoView.js b/Week3/homework/project-hack-your-repo-III/RepoView.js
new file mode 100644
index 000000000..783e4f310
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/RepoView.js
@@ -0,0 +1,48 @@
+'use strict';
+
+{
+ // DOT.NOT:JS-3:w-3:project: include additional utility functions
+ const { createAndAppend , createTableRowHTML ,
+ getValidValue , createTableCellObject } = window.Util;
+
+ class RepoView {
+ constructor(container) {
+ this.container = container;
+ // DOT.NOT:JS-3:w-3:project: additional rendering / styling elements
+ this.dataList=createAndAppend('TABLE',createAndAppend('DIV'
+ ,this.container,{id:'id_wrap_repositories'})
+ ,{id:'id_table_repositories'});
+ }
+
+ update(state) {
+ if (!state.error) {
+ this.render(state.selectedRepo);
+ }
+ }
+
+ /**
+ * Renders the repository details.
+ * @param {Object} repo A repository object.
+ */
+ render(repo) {
+ // DOT.NOT:JS-3:w-3:project: implement repository info rendering
+ const cleanDT=(dtStr)=>getValidValue(dtStr).replace(/[TZ]/g,' ').trim();
+ this.dataList.innerHTML=[
+ [ createTableCellObject('Repository','',false,true),
+ createTableCellObject(getValidValue(repo.name),
+ getValidValue(repo.html_url),true), ],
+ [ createTableCellObject('Description','',false,true),
+ createTableCellObject(getValidValue(repo.description),'',true), ],
+ [ createTableCellObject('Forks','',false,true),
+ createTableCellObject(getValidValue(repo.forks_count,0),'',true), ],
+ [ createTableCellObject('Created','',false,true),
+ createTableCellObject(cleanDT(repo.created_at),'',true), ],
+ [ createTableCellObject('Updated','',false,true),
+ createTableCellObject(cleanDT(repo.updated_at),'',true), ],
+ ].reduce((tot,cur)=>tot+createTableRowHTML(cur),'');
+ };
+
+ }
+
+ window.RepoView = RepoView;
+}
diff --git a/Week3/homework/project-hack-your-repo-III/Util.js b/Week3/homework/project-hack-your-repo-III/Util.js
new file mode 100644
index 000000000..9ba2fcc22
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/Util.js
@@ -0,0 +1,81 @@
+'use strict';
+
+{
+ class Util {
+ /**
+ * Creates an element, optionally setting its attributes, and appends
+ * the element to a parent.
+ * @param {string} name The tag name of the element to create.
+ * @param {HTMLElement} parent The parent element.
+ * @param {Object} options An object with attribute names and values.
+ */
+ static createAndAppend(name, parent, options = {}) {
+ const elem = document.createElement(name);
+ parent.appendChild(elem);
+ Object.entries(options).forEach(([key, value]) => {
+ if (key === 'text') {
+ elem.textContent = value;
+ } else {
+ elem.setAttribute(key, value);
+ }
+ });
+ return elem;
+ }
+
+ // DOT.NOT:JS-3:w-3:project: implement utility method getValidValue()
+ /**
+ * accepts 2 parameters: theVal and defVal
+ * returns defVal if theVal was null or undefined, else returns theVal
+ * @param {any} theVal The value to validate.
+ * @param {any} defVal The value to return in case theVal found invalid.
+ */
+ static getValidValue(theVal,defVal='') {
+ return (theVal==null)||(theVal==undefined)||(!theVal)?defVal:theVal;
+ };
+
+ // DOT.NOT:JS-3:w-3:project: implement utility method createTableCellObject()
+ /**
+ * accepts 4 parameters describing the data and style of a table cell
+ * returns an object that is a suitable element
+ * for the array parameter of the next method (createTableRowHTML)
+ * @param {string} aText The text of the cell.
+ * @param {string} aLink The link this cell points to (creates an anchor).
+ * @param {boolean} doBold whether to show the text bold or not.
+ * @param {boolean} doSlant whether to show the text slanted (italic) or not.
+ */
+ static createTableCellObject(aText='',aLink=null,doBold=false,doSlant=false) {
+ return { text:aText, link:aLink, bold:doBold, slant:doSlant };
+ };
+
+ // DOT.NOT:JS-3:w-3:project: implement utility method createTableRowHTML()
+ /**
+ * accepts an array of objects, each of which is describing a table cell
+ * returns a string that contains HTML structured tags
+ * describing a complete table row based on the input array
+ * and that can be assigned directly to a (table)DOMelement.innerHTML
+ * @param {array} rowData The array of cell dascription objects.
+ */
+ static createTableRowHTML(rowData=[]) {
+ return rowData.reduce((tot,cur)=>
+ tot+getCellHTML(cur.text,cur.link,cur.bold,cur.slant)
+ ,'')+' ';
+ function getCellHTML(aText,aLink=null,makeBold=false,makeItalic=false) {
+ let result=createAnchorHTML(aText,aLink);
+ if (makeBold) {result=`${result} `};
+ if (makeItalic) {result=`${result} `};
+ return `${result} `;
+ };
+ function createAnchorHTML(txt,ref) {
+ const trg='title="Tap for transition to related page." target="_blank"';
+ return noValue(ref)?txt:`${txt} `;
+ };
+ function noValue(val) {
+ return (val==undefined)||(val==null)||(String(val).trim()==='');
+ };
+ };
+
+ }
+
+ window.Util = Util;
+}
+
diff --git a/Week3/homework/project-hack-your-repo-III/hyf.png b/Week3/homework/project-hack-your-repo-III/hyf.png
new file mode 100644
index 000000000..76bc5a13b
Binary files /dev/null and b/Week3/homework/project-hack-your-repo-III/hyf.png differ
diff --git a/Week3/homework/project-hack-your-repo-III/index.html b/Week3/homework/project-hack-your-repo-III/index.html
new file mode 100644
index 000000000..b49df1eb5
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+ HYF-GITHUB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week3/homework/project-hack-your-repo-III/style.css b/Week3/homework/project-hack-your-repo-III/style.css
new file mode 100644
index 000000000..da057fbd8
--- /dev/null
+++ b/Week3/homework/project-hack-your-repo-III/style.css
@@ -0,0 +1,114 @@
+
+/* resets */
+body, div, table, tr, td, ul, li, a, p, img, span,
+header, main, section, .main-container, .whiteframe {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ cursor: default;
+ vertical-align: middle;
+}
+
+body {
+ background-color: wheat;
+ margin: 12px 12px 0;
+ font-size: 1.5rem;
+ font-family: "Roboto", sans-serif;
+}
+
+a {
+ display: inline-block;
+ color: ButtonText;
+ text-decoration: none;
+ padding: 1px 9px;
+ background: ButtonFace;
+ border-style: solid;
+ border-width: 2px;
+ border-color: silver grey grey silver;
+ appearance: button;
+ -moz-appearance: button;
+ -webkit-appearance: button;
+}
+
+a:hover {
+ background: white;
+}
+
+*:focus {
+ outline: 3px solid black;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td {
+ padding-top: 6px;
+ padding-bottom: 3px;
+}
+td:nth-child(1) {
+ padding-right: 8px;
+ vertical-align: top;
+}
+td:nth-child(2) {
+ overflow-wrap: anywhere;
+ padding-left: 8px;
+}
+td:nth-child(3) {
+ padding-left: 16px;
+ text-align: right;
+}
+
+.header {
+ background-color: steelblue;
+ margin-bottom: 8px;
+ padding: 12px;
+ box-shadow: 4px 6px 9px #888888;
+}
+
+.header > * {
+ font-size: 1.5rem;
+ max-width: 100%;
+}
+
+.header > div {
+ margin-right: 20px;
+ padding-bottom: 5px;
+ font-style: italic;
+ font-weight: bold;
+ font-size: 1.7rem;
+ display: inline-block;
+}
+
+.main-container {
+ display: grid;
+ grid-gap: 9px;
+ grid-template-columns:repeat(auto-fit,minmax(24rem,1fr));
+}
+
+#id_wrap_repositories {
+ background-color: orange;
+ padding: 8px 12px 4px;
+ box-shadow: 4px 6px 9px #888888;
+}
+
+#id_wrap_contributors {
+ background-color: steelblue;
+ padding: 14px 12px 4px;
+ box-shadow: 4px 6px 9px #888888;
+}
+
+#id_table_contributors {
+ width: 100%;
+}
+
+#id_title_contributors {
+ font-style: italic;
+ font-weight: bold;
+}
+
+.alert-error {
+ color: red;
+}
+