diff --git a/.guides/book.json b/.guides/book.json new file mode 100644 index 0000000..54a6a18 --- /dev/null +++ b/.guides/book.json @@ -0,0 +1,17 @@ +{ + "name": "Progressive Web App Template", + "children": [ + { + "title": "Progressive Web App Template", + "id": "1ec2c0d2-f676-c535-aa04-10b95b9272d6", + "pageId": "1ec2c0d2-f676-c535-aa04-10b95b9272d6", + "type": "page" + }, + { + "title": "Page 2", + "id": "00798083-5d70-f87e-6c4a-5b427dd3ab91", + "pageId": "00798083-5d70-f87e-6c4a-5b427dd3ab91", + "type": "page" + } + ] +} \ No newline at end of file diff --git a/.guides/content/Page-2-0079.md b/.guides/content/Page-2-0079.md new file mode 100644 index 0000000..ce7d2e1 --- /dev/null +++ b/.guides/content/Page-2-0079.md @@ -0,0 +1 @@ +# Sample content Page 2 \ No newline at end of file diff --git a/.guides/content/Progressive-Web-App-Template-1ec2.md b/.guides/content/Progressive-Web-App-Template-1ec2.md new file mode 100644 index 0000000..16cb903 --- /dev/null +++ b/.guides/content/Progressive-Web-App-Template-1ec2.md @@ -0,0 +1,25 @@ +This code provides a working template for the development of modern Progressive Web App that needs to be connected to a running instance of the **Rest API Template**. + +It only runs on Codio boxes. + +To install, run the following command from inside the Codio box: + +``` +$ curl -sL https://bit.ly/XXXXXXX | sudo -E bash - +``` + +> Note this will delete ALL the existing content from your Codio Box. + +The server will start when you open the terminal. If you need to start the server manually, run the following command: + +```shell +$ http-server + Starting up http-server, serving ./ + Available on: + http://127.0.0.1:8080 + http://10.177.160.215:8080 + Hit CTRL-C to stop the server +``` + +Click on the **Project Index (static)** to open the page. + diff --git a/.guides/metadata.json b/.guides/metadata.json new file mode 100644 index 0000000..f949ede --- /dev/null +++ b/.guides/metadata.json @@ -0,0 +1,40 @@ +{ + "sections": [ + { + "id": "1ec2c0d2-f676-c535-aa04-10b95b9272d6", + "title": "Progressive Web App Template", + "files": [], + "path": [], + "type": "markdown", + "content-file": ".guides/content/Progressive-Web-App-Template-1ec2.md", + "chapter": false, + "reset": [], + "teacherOnly": false, + "learningObjectives": "" + }, + { + "id": "00798083-5d70-f87e-6c4a-5b427dd3ab91", + "title": "Page 2", + "files": [], + "path": [], + "type": "markdown", + "content-file": ".guides/content/Page-2-0079.md", + "chapter": false, + "reset": [], + "teacherOnly": false, + "learningObjectives": "" + } + ], + "theme": "light", + "scripts": [], + "lexikonTopic": "", + "suppressPageNumbering": false, + "useSubmitButtons": true, + "useMarkAsComplete": true, + "hideMenu": false, + "allowGuideClose": false, + "collapsedOnStart": false, + "hideSectionsToggle": false, + "hideBackToDashboard": false, + "protectLayout": false +} \ No newline at end of file diff --git a/.settings b/.settings new file mode 100644 index 0000000..dcd0a8d --- /dev/null +++ b/.settings @@ -0,0 +1,67 @@ +[editor] +tab_size = 2 +; enter your editor preferences here... + +[ide] +; enter your ide preferences here... + +[code-beautifier] +; enter your code-beautifier preferences here... + +[codio-filetree] +; enter your codio-filetree preferences here... + +[view-javascript-code] +; enter your view-javascript-code preferences here... + +[codio-lsp] +; enter your codio-lsp preferences here... + +[junit] +; enter your junit preferences here... + +[git] +; enter your git preferences here... + +[terminal] +; enter your terminal preferences here... + +[preview] +; enter your preview preferences here... + +[emmet] +; enter your emmet preferences here... + +[search] +; enter your search preferences here... + +[guides] +; enter your guides preferences here... + +[settings] +; enter your settings preferences here... + +[account] +; enter your account preferences here... + +[project] +; enter your project preferences here... + +[install-software] +; enter your install-software preferences here... + +[deployment] +; enter your deployment preferences here... + +[container] +; enter your container preferences here... + +[sync-structure] +; enter your sync-structure preferences here... + +[education] +; enter your education preferences here... + +[Codio Lexikon] +; enter your Codio Lexikon preferences here... + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d503bc1 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ + +# Progressive Web App Template + +This code provides a working template for the development of modern Progressive Web App that needs to be connected to a running instance of the **Rest API Template**. + +It only runs on Codio boxes. + +To install, run the following command from inside the Codio box: + +``` +$ curl -sL https://bit.ly/XXXXXXX | sudo -E bash - +``` + +> Note this will delete ALL the existing content from your Codio Box. + +The server will start when you open the terminal. If you need to start the server manually, run the following command: + +```shell +$ http-server + Starting up http-server, serving ./ + Available on: + http://127.0.0.1:8080 + http://10.177.160.215:8080 + Hit CTRL-C to stop the server +``` + +Click on the **Project Index (static)** to open the page. diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..47c858a --- /dev/null +++ b/css/style.css @@ -0,0 +1,273 @@ + +/* style.css */ + +body, p, h1, h2, h3, ul { + margin: 0; + padding: 0; + font-family: Arial, Helvetica, sans-serif; +} + +p { + margin-bottom: 0.6em; + font-size: 1em; +} + +h1 { + margin-bottom: 0.5em; + font-size: 1.6em; + color: #555; +} + +header { + width: 100%; + background-color: #CCC; + padding: 1em; + padding-bottom: 0.5em; +} + +header p { + font-size: 0.9em; + color: #555; + margin: 0.2em; +} + +header strong { + color: green; +} + +.offline { + color: red; +} + +main { + padding: 1em; +} + +footer { + position: absolute; + bottom: 0; + background-color: #DDD; + width: 100%; + padding: 0; +} + +input { + width: 200px; + font-size: 1.2em; +} + +ul { + padding: 0; + margin: 0; + list-style-type: none; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + background-color: #CCCC; + /* height: 3em; */ + justify-content: flex-start; +} + +li { + padding: 0; + margin: 0; + display: inline-block; + flex-basis: 8em; + /* border: 1px solid grey; */ + height: 2em; + text-align: center; + text-transform: uppercase; + padding-top: 1em; + font-size: 0.8em; +} + +li > a { + text-decoration: none; + color: black; +} + +li:hover a { + color: white; +} + +li:hover { + background-color: grey; +} + +aside { + border: 0.1em solid red; + padding: 1em; + margin: 1em; + color: red; + font-size: 1em; + border-radius: 0.5em; +} + +.hidden { + display: none; +} + +button { + background-color: grey; + border: 2px solid grey; + color: white; + padding: 0.5em 1em; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 0.8em; + margin-top: 0.5em; +} + +button:hover { + color: grey; + background-color: white; +} + +input[type="text"], input[type="email"], input[type="password"], textarea, select { + font-size: 0.8em; + width: 300px; + border: 1px solid #CCC; + padding: 0.5em; + margin-top: 0.3em; +} +main > button { + position: fixed; + z-index: 2; + bottom: 0.5em; + right: 0.5em; +} + +main > section { + display: absolute; + top: 15em; + width: 315px; + border: 2px solid #CCC; + padding: 1em; + padding-bottom: 3em; + margin: auto; +} + +main > section h2 { + margin-bottom: 0.5em; + color: #555; +} + +main > section button { + float: right; +} + +article { + background-color: #CCCC; + margin-bottom: 0.5em; + padding: 1em; + padding-bottom: 3em; +} + +article > h2 { + margin: 0; + padding: 0; + /* display: inline-block; */ + font-size: 1.2em; + /* background-color: blue; */ + height: 2em; + flex-grow: 1; +} + +article > p { + margin: 0; + padding: 0; + /* display: inline-block; */ + /* background-color: yellow; */ + height: 2em; + flex-grow: 2; +} + +article > button { + margin: 0; + display: block; + height: 3em; + flex-grow: 1; + float: right; +} + +footer { + position: fixed; + bottom: 0; + left: 0; + background-color: #DDDD; + width: 100%; + padding: 0; + margin: 0; + padding-left: 0.3em; + padding-top: 0.5em; +} + +footer > p#device, footer > p#resolution, footer > p#connectivity { + display: inline-block; + height: 1.5em; + width: 1.5em; + margin: 0; + margin-top: 0.3em; + margin-bottom: 0.3em; + background-size: contain; +} + +footer > p#connectivity { + background-image: url(../images/online.png); +} + +footer > p#location { + display: inline-block; + margin: 0; + height: 0.7em; + margin-left: 0.3em; + font-size: 0.8em; + color: gray; +} + +/* ==================== DETECTS IF SCREEN SUPPORTS TOUCH ==================== */ + +/* Detects a touch-screen device */ +@media (hover: none) and (pointer: coarse) { + footer > p#device { + background-image: url(../images/tablet.png); + background-size: contain; + } + +} + +/* Detects a non-touch screen with a mouse or trackpad */ +@media (hover: hover) and (pointer: fine) { + footer > p#device { + background-image: url(../images/desktop.png); + background-size: contain; + } +} + +/* ======================== DETECTS THE SCREEN WIDTH ======================== */ + +/* Narrow screen */ +@media (max-width: 400px) { + footer > p#resolution { + background-image: url(../images/red.png); + background-size: contain; + } +} + +/* Medium width screen */ +@media (min-width: 401px) and (max-width: 800px) { + footer > p#resolution { + background-image: url(../images/amber.png); + background-size: contain; + } +} + +/* Wide screen */ +@media (min-width: 801px) { + footer > p#resolution { + background-image: url(../images/green.png); + background-size: contain; + } +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..d8b7779 Binary files /dev/null and b/favicon.ico differ diff --git a/images/amber.png b/images/amber.png new file mode 100644 index 0000000..3f49208 Binary files /dev/null and b/images/amber.png differ diff --git a/images/desktop.png b/images/desktop.png new file mode 100644 index 0000000..f615779 Binary files /dev/null and b/images/desktop.png differ diff --git a/images/green.png b/images/green.png new file mode 100644 index 0000000..499f166 Binary files /dev/null and b/images/green.png differ diff --git a/images/offline.png b/images/offline.png new file mode 100644 index 0000000..755791e Binary files /dev/null and b/images/offline.png differ diff --git a/images/online.png b/images/online.png new file mode 100644 index 0000000..5218fd3 Binary files /dev/null and b/images/online.png differ diff --git a/images/red.png b/images/red.png new file mode 100644 index 0000000..89496c7 Binary files /dev/null and b/images/red.png differ diff --git a/images/tablet.png b/images/tablet.png new file mode 100644 index 0000000..8394aca Binary files /dev/null and b/images/tablet.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..67a5603 --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + + + Simple SPA + + + + + + + + +
+

+

You are currently online

+
+ + +
+ +
+ + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..a6c6cd4 --- /dev/null +++ b/index.js @@ -0,0 +1,17 @@ + +/* index.js */ + +import Koa from 'koa' +import https from 'https' + +const app = new Koa() + +const port = 8080 + +app.use(async ctx => { + ctx.body = 'Hello World' +}) + +app.listen(port, () => { + console.log(`listening on port ${port}`) +}) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..459118a --- /dev/null +++ b/install.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# SETUP RESTful API TEMPLATE + +red=`tput setaf 1` +green=`tput setaf 2` +reset=`tput sgr0` + +echo +echo "======= CHECKING WE ARE ON A CODIO BOX =======" +if [ -v CODIO_HOSTNAME ] +then + echo "Codio box detected" + echo "continuing setup" +else + echo "no Codio box detected" + echo "exiting setup" + exit 1 +fi +sudo chown -R codio:codio . + +# destructive section start + +# echo +# echo "============== ${green}DELETING${reset} OLD FILES ===================" +# rm -rf * +# rm -rf .* +# rm -rf .guides +# rm -rf .git +# echo +# echo "============== CLONING ${green}REPOSITORY${reset} ===================" +# git clone https://github.coventry.ac.uk/web/Codio-API-Template.git . +# git remote rm origin +# rm -rf install.sh # delete this script so it can't be run from inside the project! +# rm .codio +# mv codio.json .codio +# echo +# echo "============= DELETING ${green}TEMPORARY FILES${reset} ==============" +# rm -rf *.db # delete any old database files +# rm -rf package-lock.json +# rm -rf .settings +# rm -rf .sqlite_history +# rm -rf .bash_history +# rm -rf .git # delete the repository we have cloned (if any) + +# end of distructive section + +echo +echo "============ INSTALLING PACKAGES ============" + +sudo add-apt-repository -y ppa:git-core/ppa +sudo apt update -y +sudo apt upgrade -y + +sudo apt install -y psmisc lsof tree build-essential gcc g++ make jq curl git +sudo apt autoremove -y + +echo +echo "========= INSTALLING NODE USING ${green}NODESOURCE${reset} =========" +curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - +sudo apt install -y nodejs +echo +echo "=========== INSTALLING THE ${green}NODE PACKAGES${reset} ===========" +echo +rm -rf node_modules +rm -rf package-lock.json +sudo npm install -g http-server + +echo +echo "========= CUSTOMISING SHELL PROMPT ==========" +if grep PS1 ~/.profile +then + echo "correct prompt found" +else + echo "prompt needs updating" + echo "PS1='$ '" >> ~/.profile +fi + +if grep http-server ~/.profile +then + echo "server startup script found" +else + echo "adding server startup script" + echo "http-server" >> ~/.profile +fi + +source ~/.profile diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..b185d77 --- /dev/null +++ b/js/core.js @@ -0,0 +1,61 @@ + +/* core.js */ + +export function generateToken(user, pass) { + const token = `${user}:${pass}` + const hash = btoa(token) + return `Basic ${hash}` +} + +export async function login() { + if(!getCookie('authorization')) throw new Error('cookie not found') + const options = { headers: { Authorization: getCookie('authorization') } } + const response = await fetch('/login',options) + const status = response.status + console.log(`HTTP status code: ${status}`) + if(response.status === 401) throw new Error('status 401 NOT AUTHORIZED') +} + +// from plainjs.com +export function setCookie(name, value, days) { + const d = new Date + d.setTime(d.getTime() + 24*60*60*1000*days) + document.cookie = `${name}=${value};path=/;expires=${d.toGMTString()}` +} + +export function getCookie(name) { + const v = document.cookie.match(`(^|;) ?${name}=([^;]*)(;|$)`) + return v ? v[2] : null +} + +export function deleteCookie(name) { + setCookie(name, '', -1) +} + +export function onlineStatus() { + if(navigator.onLine) { + return true + } else { + return false + } +} + +export function showMessage(message) { + console.log(message) + document.querySelector('aside p').innerText = message + document.querySelector('aside').classList.remove('hidden') + setTimeout( () => document.querySelector('aside').classList.add('hidden'), 2000) +} + +export function getLocation() { + if(navigator.geolocation) { + console.log('location supported') + navigator.geolocation.getCurrentPosition( position => { + const pos = position.coords + const locString = `lat: ${pos.latitude}, lon: ${pos.longitude}` + document.getElementById('location').innerHTML = `${locString}
 ` + }) + } else { + console.log('geolocation not supported') + } +} diff --git a/js/script.js b/js/script.js new file mode 100644 index 0000000..7edc3d7 --- /dev/null +++ b/js/script.js @@ -0,0 +1,52 @@ + +/* script.js */ + +/** + * ROUTER + * This module needs to be imported into your top-level html file. + * It checks the URL fragment/hash and dynamically loads the correct view + * and module. + * There needs to be an html view file and a module file for each fragment. + */ + +import { getCookie, getLocation, onlineStatus } from './core.js' + +let geoID + +// event triggered when the page first loads, triggers the 'hashchange' event +window.addEventListener('DOMContentLoaded', async event => { +// geoID = await navigator.geolocation.watchPosition(getLocation) + loadPage() +}) + +// event gets triggered every time the URL fragment (hash) changes in the address bar +window.addEventListener('hashchange', async event => await loadPage()) + +async function loadPage() { + try { + // the 'name' of the page is the name in the fragment without the # character + // if there is no fragment/hash, assume we want to load the home page (ternary operator) + console.log(typeof location.hash) + const pageName = location.hash ? location.hash.replace('#', '') : 'home' + console.log('location updated') + // load the html page that matches the fragment and inject into the page DOM + document.querySelector('main').innerHTML = await (await fetch(`./views/${pageName}.html`)).text() + document.querySelector('h1').innerText = pageName + console.log('---------------------------------------------------------------') + console.log(`${pageName.toUpperCase()}: ${window.location.protocol}//${window.location.host}/${window.location.hash}`) + if(getCookie('authorization')) console.log(`Authorization: "${getCookie('authorization')}"`) + // dynamically importing the code module who's name matches the page name + try { + // run the setup function in whatever module has been loaded if module exists + const module = await import(`../modules/${pageName}.js`) + module.setup() + } catch(err) { + console.warn('no page module') + } + } catch(err) { + // errors are triggered if script can't find matching html fragment or script + console.log(err) + console.log(`error: ${err.message}, redirecting to 404 page`) + console.log(err) + } +} diff --git a/modules/home.js b/modules/home.js new file mode 100644 index 0000000..d5c044a --- /dev/null +++ b/modules/home.js @@ -0,0 +1,21 @@ + +/* home.js */ + +import { getCookie, login, showMessage } from '../js/core.js' + +const apiURL = 'https://arizona-spray-8080.codio-box.uk' + +export async function setup() { + try { + console.log('MAIN SCRIPT') + const url = `${apiURL}/accounts` + console.log(url) + const json = await fetch(url) + const data = await json.json() + console.log(data) + document.querySelector('main p').innerText = data.msg + } catch(err) { + console.log(err) + window.location.href = '/#login' + } +} diff --git a/modules/login.js b/modules/login.js new file mode 100644 index 0000000..1a48697 --- /dev/null +++ b/modules/login.js @@ -0,0 +1,40 @@ + +/* login.js */ + +import { generateToken, getCookie, setCookie, showMessage, getLocation } from '../js/core.js' + +export function setup() { + const cookie = getCookie('authorization') + if(getCookie('authorization')) { + console.log('authorised') + window.location.href = '/#home' + } + document.querySelector('h1').innerText = 'Log In' + document.querySelector('form').addEventListener('submit', async event => await login(event)) +} + +async function login() { + try { + event.preventDefault() + const elements = [...document.forms['login'].elements] + const data = {} + elements.forEach( el => { + if(el.name) data[el.name] = el.value + }) + const token = generateToken(data.email, data.pass) + console.log(token) + const options = { headers: { Authorization: token } } + const response = await fetch('/login',options) + const json = await response.json() + console.log(json) + const status = response.status + console.log(`HTTP status code: ${response.status}`) + if(response.status === 401) throw new Error(json.msg) + if(response.status === 200) { + setCookie('authorization', token, 1) + window.location.href = '#home' + } + } catch(err) { + showMessage(err.message) + } +} diff --git a/modules/logout.js b/modules/logout.js new file mode 100644 index 0000000..4b73359 --- /dev/null +++ b/modules/logout.js @@ -0,0 +1,11 @@ + +/* logout.js */ + +import { deleteCookie, getCookie } from '../js/core.js' + +export async function setup() { + navigator.geolocation.clearWatch(getCookie('geoID')) + deleteCookie('authorization') + deleteCookie('geoID') + window.location.href = '/#login' +} diff --git a/modules/register.js b/modules/register.js new file mode 100644 index 0000000..64d7324 --- /dev/null +++ b/modules/register.js @@ -0,0 +1,26 @@ + +/* register.js */ + +import { showMessage } from '../js/core.js' + +export async function setup() { + document.querySelector('h1').innerText = 'Register a New Account' + document.querySelector('form').addEventListener('submit', await registerAccount) +} + +async function registerAccount(event) { + event.preventDefault() + try { + const elements = [...document.forms['register'].elements] + const data = {} + elements.forEach( el => { if(el.name) data[el.name] = el.value }) + console.log(data) + const options = { method: 'post', body: JSON.stringify(data) } + const response = await fetch('/register',options) + const json = await response.json() + if(response.status === 422) throw new Error(`422 Unprocessable Entity: ${json.msg}`) + window.location.href = '/#login' + } catch(err) { + showMessage(err.message) + } +} diff --git a/views/404.html b/views/404.html new file mode 100644 index 0000000..8e6f1ef --- /dev/null +++ b/views/404.html @@ -0,0 +1,2 @@ + +

404 PAGE NOT FOUND!

diff --git a/views/home.html b/views/home.html new file mode 100644 index 0000000..fd4b960 --- /dev/null +++ b/views/home.html @@ -0,0 +1,2 @@ + +

diff --git a/views/login.html b/views/login.html new file mode 100644 index 0000000..f1fab4a --- /dev/null +++ b/views/login.html @@ -0,0 +1,6 @@ + +
+


+


+

+
diff --git a/views/logout.html b/views/logout.html new file mode 100644 index 0000000..b3d5339 --- /dev/null +++ b/views/logout.html @@ -0,0 +1,2 @@ + +

Log Out

diff --git a/views/register.html b/views/register.html new file mode 100644 index 0000000..992e4ad --- /dev/null +++ b/views/register.html @@ -0,0 +1,7 @@ + +
+


+


+


+

+