├── .gitignore ├── _assets ├── .DS_Store ├── img │ ├── marker-shadow.png │ ├── marker-icon-2x-black.png │ ├── marker-icon-2x-blue.png │ ├── marker-icon-2x-gold.png │ ├── marker-icon-2x-green.png │ ├── marker-icon-2x-grey.png │ ├── marker-icon-2x-red.png │ ├── marker-icon-2x-orange.png │ ├── marker-icon-2x-violet.png │ └── marker-icon-2x-yellow.png ├── css │ ├── map.css │ └── form.css └── js │ ├── form.js │ ├── particles.js │ └── map.js ├── config.sample.js ├── map └── index.html ├── readme.md └── form └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | config.js -------------------------------------------------------------------------------- /_assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/.DS_Store -------------------------------------------------------------------------------- /_assets/img/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-shadow.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-black.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-blue.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-gold.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-green.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-grey.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-red.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-orange.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-violet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-violet.png -------------------------------------------------------------------------------- /_assets/img/marker-icon-2x-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/events-google-form/master/_assets/img/marker-icon-2x-yellow.png -------------------------------------------------------------------------------- /config.sample.js: -------------------------------------------------------------------------------- 1 | var CONFIG = { 2 | map: { 3 | lat: 50.85, 4 | lon: -0.2, 5 | zoom: 12 6 | }, 7 | form: { 8 | id: "[Google form ID]", 9 | name: "[input name]", 10 | url: "[input name]", 11 | type: "[input name]", 12 | date: "[input name]", 13 | starttime: "[input name]", 14 | endtime: "[input name]", 15 | address: "[input name]", 16 | postcode: "[input name]", 17 | lat: "[input name]", 18 | lon: "[input name]", 19 | visible: "[input name]" 20 | }, 21 | spreadsheet: { 22 | id: "[Google spreadsheet ID]", 23 | apiKey: "[Google spreadsheet api key]", 24 | sheet: "[Google spreadsheet sheet name]", 25 | range: "[Start cell]:[End cell]" 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
NameTypeDateAddress
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Events with Google Forms 2 | 3 | A custom HTML form that stores data in a Google Form, and uses this information to generate an event map using [Leaflet](https://leafletjs.com/). 4 | 5 | 6 | ## Set-up 7 | 8 | Create a config file by duplicating the file `config.sample.js` and calling it `config.js`. 9 | 10 | Customise the three blocks of values: 11 | 12 | 13 | ### Map 14 | 15 | - `lat`, `lon` and `zoom`: These are the default position for the map. 16 | 17 | 18 | ### Form 19 | 20 | Create a new Google Form. 21 | 22 | The `id` field in the config should come from the URL of the form. 23 | 24 | The form should have the following fields - all defined as text fields (Short answer or long answer): 25 | 26 | - Name 27 | - URL 28 | - Event type 29 | - Date 30 | - Start time 31 | - End time 32 | - Address 33 | - Postcode 34 | - Latitude (optional) 35 | - Longitude (optional) 36 | - Visible 37 | 38 | Lat and Lon fields must be optional. They won't be filled in by any users. Instead, [Postcodes.io](https://postcodes.io/) is used to geocode the postcode and retrieve a latitude and longitude. 39 | 40 | The inputs *must* be in the order specified above. 41 | 42 | Find out the IDs for these input fields to add to the config. The easiest way to do this is to use the browser's webdev inspector. See [this link](https://github.com/jsdevel/google-form) for detail. They will probably be named `entry.xxxxx`. 43 | 44 | 45 | ### Spreadsheet 46 | 47 | Create a Google Spreadsheet from the Google Form responses. 48 | 49 | Make the spreadsheet visible publicly - read only. 50 | 51 | The `id` field in the config should come from the URL of the spreadsheet. 52 | 53 | Sign up for a new Google Spreadsheet API key - see [their documentation](https://developers.google.com/sheets/api/quickstart/js) for detail. You don't need a Client ID, just the [API key](https://console.developers.google.com/apis/credentials). 54 | 55 | While setting this up, it's worth restricting its use to specific domains, and only the _Google Sheets API_. 56 | 57 | The `sheet` field in the config should come from the name of the spreadsheet tab - this will be created automatically by the Google Form. Replace any spaces with _%20_, for example the value may be `Form%20Responses%201`. 58 | 59 | The `range` field in the config should include the fields that are populated by the form, for example `A2:L10000`. 60 | 61 | 62 | --- 63 | 64 | ## Maintenance and support 65 | 66 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 67 | 68 | --- 69 | 70 | ## License 71 | 72 | This work is free. You can redistribute it and/or modify it under the 73 | terms of the Do What The Fuck You Want To Public License, Version 2, 74 | as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. 75 | 76 | ``` 77 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 78 | Version 2, December 2004 79 | 80 | Copyright (C) 2004 Sam Hocevar 81 | 82 | Everyone is permitted to copy and distribute verbatim or modified 83 | copies of this license document, and changing it is allowed as long 84 | as the name is changed. 85 | 86 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 87 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 88 | 89 | 0. You just DO WHAT THE FUCK YOU WANT TO. 90 | 91 | ``` -------------------------------------------------------------------------------- /form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Event form 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |

Curiosity Sussex

22 |

Please add your event

23 |
24 | 25 |
26 |
27 |
28 | 31 | 32 |
33 |
34 | 37 | 38 |
39 |
40 | 43 |
44 | 51 |
52 |
53 |
54 | 57 | 58 |
59 |
60 | 63 | 64 |
65 |
66 | 69 | 70 |
71 | 72 |
73 | 76 | 77 |
78 | 79 |
80 | 83 | 84 |
85 | 86 | 87 |
88 | 89 | 90 |
91 |
92 | 93 |
94 |

Thank you!

95 |

You will be added to our map shortly...

96 |
97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /_assets/css/map.css: -------------------------------------------------------------------------------- 1 | /* Make the map fill the window */ 2 | /* Defaults */ 3 | 4 | html { 5 | box-sizing:border-box; 6 | } 7 | 8 | *, 9 | :after, 10 | :before { 11 | box-sizing:inherit; 12 | } 13 | 14 | html, 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | height: 100%; 19 | } 20 | 21 | html { 22 | font-family: Helvetica, sans-serif; 23 | font-size: 16px; 24 | line-height: 1.5; 25 | } 26 | 27 | .content { 28 | height: 100%; 29 | display: flex; 30 | flex-direction: column; 31 | } 32 | 33 | .content-map { 34 | height: 100%; 35 | position: relative; 36 | } 37 | 38 | .content.show-table .content-map { 39 | height: 50%; 40 | } 41 | 42 | .content-table { 43 | height: 0; 44 | overflow: hidden; 45 | } 46 | 47 | .content.show-table .content-table { 48 | height: 50%; 49 | overflow-y: scroll; 50 | padding: 1em; 51 | } 52 | 53 | /* Set the map height to define the size of the map element */ 54 | 55 | #map { 56 | height: 100%; 57 | position: relative; 58 | z-index: 1; 59 | } 60 | 61 | .leaflet-tile-pane { 62 | filter: grayscale(100%); 63 | } 64 | 65 | /* popup content */ 66 | .leaflet-popup-content .popup-content p { 67 | margin: 0; 68 | } 69 | 70 | .popup-name { 71 | font-weight: bold; 72 | font-size: 1rem; 73 | } 74 | 75 | .popup-date { 76 | font-size: 0.8rem; 77 | } 78 | 79 | /* stats */ 80 | 81 | .stats { 82 | position: absolute; 83 | bottom: 1rem; 84 | left: 1rem; 85 | z-index: 2; 86 | } 87 | 88 | .table-toggle { 89 | -webkit-appearance: none; 90 | -moz-appearance: none; 91 | border: none; 92 | cursor: pointer; 93 | margin: 0; 94 | padding: 1rem 2rem; 95 | background: rgba(255, 255, 255, 0.6); 96 | font-size: 1.2rem; 97 | font-weight: bold; 98 | } 99 | 100 | .table-toggle:hover { 101 | background: rgba(255, 255, 255, 0.9); 102 | } 103 | 104 | /* table */ 105 | 106 | .event-table-container {} 107 | 108 | .event-table { 109 | border-collapse: collapse; 110 | border-color: #fff; 111 | border-spacing: 0; 112 | border-width: 0; 113 | display: inline-block; 114 | margin: 1em 0 1em 0; 115 | max-width: 100%; 116 | overflow-x: auto; 117 | white-space: nowrap; 118 | width: 100%; 119 | } 120 | 121 | @media (min-width: 62.5em) { 122 | .event-table { 123 | display: table; 124 | white-space: inherit; 125 | } 126 | } 127 | 128 | .event-table thead, 129 | .event-table tfoot { 130 | background: #036; 131 | color: #fff; 132 | text-align: left; 133 | } 134 | 135 | .event-table th { 136 | border-bottom: 2px solid #fff; 137 | padding: 1em; 138 | text-align: left; 139 | border: 1px solid #fff; 140 | padding: 1em; 141 | vertical-align: top; 142 | } 143 | 144 | .event-table td { 145 | background: #fff; 146 | border-bottom: 2px solid #fff; 147 | color: #036; 148 | padding: 1em; 149 | } 150 | 151 | .event-table tr:nth-child(even) td { 152 | background-color: #efefef; 153 | } 154 | 155 | /* log */ 156 | 157 | .log { 158 | position: absolute; 159 | bottom: 1rem; 160 | left: 33%; 161 | right: 33%; 162 | z-index: 2; 163 | margin: 0; 164 | max-height: 6rem; 165 | overflow: scroll; 166 | text-align: center; 167 | list-style: none; 168 | } 169 | 170 | .log li { 171 | background: rgba(255, 255, 255, 0.6); 172 | width: auto; 173 | margin-bottom: 0.2rem; 174 | opacity: 0; 175 | animation: log 6s linear 1; 176 | } 177 | 178 | @keyframes log { 179 | 0%, 75% { opacity: 1; } 180 | 100% { opacity: 0; } 181 | } 182 | 183 | /* timer */ 184 | 185 | .timer { 186 | position: absolute; 187 | top: 0; 188 | left: 0; 189 | right: 0; 190 | background: rgba(0, 0, 0, 0.3); 191 | height: 0.5rem; 192 | z-index: 2; 193 | } 194 | 195 | .timer.is-waiting::before { 196 | content: ""; 197 | position: absolute; 198 | width: 50%; 199 | left: 0; 200 | top: 0; 201 | height: 100%; 202 | background: rgba(255, 128, 0, 0.8); 203 | 204 | animation-name: timer; 205 | animation-duration: 30s; 206 | animation-timing-function: linear; 207 | animation-delay: 0s; 208 | animation-direction: normal; 209 | animation-iteration-count: 1; 210 | animation-fill-mode: forwards; 211 | animation-play-state: running; 212 | } 213 | 214 | @keyframes timer { 215 | from { width: 0; } 216 | to { width: 100%; } 217 | } -------------------------------------------------------------------------------- /_assets/js/form.js: -------------------------------------------------------------------------------- 1 | (function(config) { 2 | 3 | // Use Google Forms for data storage 4 | // Geocode postcode data using https://postcodes.io/ 5 | // 6 | // https://github.com/jsdevel/google-form 7 | // https://www.codeproject.com/tips/721795/store-your-form-data-in-google-spreadsheet 8 | 9 | var wrapperEl; 10 | var formEl; 11 | var cancelEl; 12 | var inputEls; 13 | var selectEl; 14 | var postcodeEl; 15 | 16 | function init() { 17 | wrapper = document.querySelector('.Wrapper'); 18 | formEl = document.querySelector('.Form'); 19 | submitEl = formEl.querySelector('.Form-submit'); 20 | cancelEl = formEl.querySelector('.Form-cancel'); 21 | inputEls = formEl.querySelectorAll('.Form-input, .Form-textarea, .Form-select'); 22 | selectEl = formEl.querySelector('.Form-select'); 23 | postcodeEl = formEl.querySelector('#postcode'); 24 | 25 | cancelEl.addEventListener('click', resetForm); 26 | formEl.addEventListener('submit', handleFormSubmit); 27 | 28 | inputEls.forEach(function(inputEl) { 29 | inputEl.addEventListener('input', checkInputValidity); 30 | }); 31 | 32 | selectEl.addEventListener('change', checkInputValidity); 33 | 34 | inputEls[0].focus(); 35 | } 36 | 37 | function handleFormSubmit(e) { 38 | e.preventDefault(); 39 | convertPostcodeToLatLon(); 40 | } 41 | 42 | function checkInputValidity() { 43 | var valid = true; 44 | inputEls.forEach(function(inputEl) { 45 | if (!inputEl.validity.valid) { 46 | valid = false; 47 | } 48 | }); 49 | submitEl.disabled = !valid; 50 | } 51 | 52 | // attempt geocoding for postcode - but store response regardless of success 53 | function convertPostcodeToLatLon() { 54 | var postcode = postcodeEl.value; 55 | 56 | var url = 'https://api.postcodes.io/postcodes/' + postcode; 57 | 58 | request = new XMLHttpRequest(); 59 | request.open('GET', url, true); 60 | request.send(); 61 | request.onreadystatechange = handlePostcodeResponse; 62 | } 63 | 64 | function handlePostcodeResponse() { 65 | if (this.readyState === 4) { 66 | var latLon = null; 67 | 68 | // carry on anyway, but if we get a lat/lon then use it 69 | if (this.status === 200 || this.status === 0) { 70 | var response = JSON.parse(this.response); 71 | if (response.result && response.result.latitude && response.result.longitude) { 72 | latLon = { 73 | 'lat': response.result.latitude, 74 | 'lon': response.result.longitude 75 | } 76 | } 77 | } 78 | 79 | prepareFormData(latLon); 80 | } 81 | } 82 | 83 | function prepareFormData(latLon) { 84 | var data = ['submit=Submit']; 85 | 86 | if (latLon) { 87 | data.push(config.form.lat + '=' + latLon.lat); 88 | data.push(config.form.lon + '=' + latLon.lon); 89 | } 90 | 91 | inputEls.forEach(function(inputEl) { 92 | if (inputEl.classList.contains("Form-select")) { 93 | var value = inputEl.options[inputEl.selectedIndex].value; 94 | data.push(config.form[inputEl.id] + '=' + encodeURIComponent(value)); 95 | } else { 96 | data.push(config.form[inputEl.id] + '=' + encodeURIComponent(inputEl.value)); 97 | } 98 | }); 99 | 100 | // add default visibility 101 | data.push(config.form.visible + '=false'); 102 | 103 | var url = "https://docs.google.com/forms/d/" + config.form.id + '/formResponse?' + data.join('&'); 104 | 105 | processFormData(url, data); 106 | } 107 | 108 | function processFormData(url, data) { 109 | request = new XMLHttpRequest(); 110 | request.open('GET', url, true); 111 | request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 112 | request.send(); 113 | request.onreadystatechange = handleFormResponse; 114 | } 115 | 116 | function handleFormResponse() { 117 | if (this.readyState === 4) { 118 | // show success state regardless, but if there was an error we'd know here 119 | if (this.status !== 200 && this.status !== 0) { 120 | console.log(this); 121 | } 122 | 123 | showSuccess(); 124 | } 125 | } 126 | 127 | function showSuccess() { 128 | formEl.classList.add('is-complete'); 129 | setTimeout(resetForm, 2000); 130 | } 131 | 132 | function resetForm() { 133 | formEl.classList.remove('is-complete'); 134 | formEl.reset(); 135 | checkInputValidity(); 136 | inputEls[0].focus(); 137 | } 138 | 139 | document.addEventListener('DOMContentLoaded', init); 140 | 141 | })(window.CONFIG); 142 | -------------------------------------------------------------------------------- /_assets/js/particles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Particles 3 | * © 2008 4 | */ 5 | (function () { 6 | 7 | "use strict"; 8 | 9 | /* 10 | * global variables 11 | */ 12 | var 13 | // HTML canvas element 14 | canvas, 15 | 16 | // canvas draw context 17 | ctx, 18 | 19 | // collection of existing particles 20 | particles = [], 21 | 22 | // configurable options 23 | config = { 24 | 25 | // number of particles to draw 26 | particleCount : 50, 27 | 28 | // minimum distance for each particle to affect another 29 | minimumAffectingDistance : 50 30 | }; 31 | 32 | /* 33 | * init 34 | */ 35 | function init () { 36 | drawCanvas(); 37 | createParticles(); 38 | loop(); 39 | 40 | // resize canvas on page resize 41 | window.addEventListener("resize", function (event) { 42 | drawCanvas(); 43 | }); 44 | } 45 | 46 | 47 | /* 48 | * start redraw loop logic 49 | */ 50 | function loop () { 51 | clear(); 52 | update(); 53 | draw(); 54 | queue(); 55 | } 56 | 57 | /* 58 | * wipe canvas ready for next redraw 59 | */ 60 | function clear () { 61 | ctx.clearRect(0, 0, canvas.width, canvas.height); 62 | } 63 | 64 | /* 65 | * update particle positions 66 | */ 67 | function update () { 68 | 69 | // update each particle's position 70 | for (var count = 0; count < particles.length; count++) { 71 | 72 | var p = particles[count]; 73 | 74 | // Change the velocities 75 | p.x += p.vx; 76 | p.y += p.vy; 77 | 78 | // Bounce a particle that hits the edge 79 | if(p.x + p.radius > canvas.width || p.x - p.radius < 0) { 80 | p.vx = -p.vx; 81 | } 82 | 83 | if(p.y + p.radius > canvas.height || p.y - p.radius < 0) { 84 | p.vy = -p.vy; 85 | } 86 | 87 | // Check particle attraction 88 | for (var next = count + 1; next < particles.length; next++) { 89 | var p2 = particles[next]; 90 | calculateDistanceBetweenParticles(p, p2); 91 | } 92 | } 93 | } 94 | 95 | /* 96 | * update visual state - draw each particle 97 | */ 98 | function draw () { 99 | for (var count = 0; count < particles.length; count++) { 100 | var p = particles[count]; 101 | p.draw(); 102 | } 103 | } 104 | 105 | /* 106 | * prepare next redraw when the browser is ready 107 | */ 108 | function queue () { 109 | window.requestAnimationFrame(loop); 110 | } 111 | 112 | // go! 113 | init(); 114 | 115 | 116 | /* 117 | * Objects 118 | */ 119 | 120 | /* 121 | * Particle 122 | */ 123 | function Particle () { 124 | 125 | // Position particle 126 | this.x = Math.random() * canvas.width; 127 | this.y = Math.random() * canvas.height; 128 | 129 | // Give particle velocity, between -1 and 1 130 | this.vx = -1 + Math.random() * 2; 131 | this.vy = -1 + Math.random() * 2; 132 | 133 | // Give particle a radius 134 | this.radius = 4; 135 | 136 | // draw particle 137 | this.draw = function () { 138 | ctx.fillStyle = "white"; 139 | ctx.beginPath(); 140 | ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false); 141 | ctx.fill(); 142 | } 143 | } 144 | 145 | /* 146 | * Draw canvas 147 | */ 148 | function drawCanvas () { 149 | canvas = document.querySelector("canvas"); 150 | ctx = canvas.getContext("2d"); 151 | 152 | // set canvas to full page dimensions 153 | canvas.width = window.innerWidth; 154 | canvas.height = window.innerHeight; 155 | } 156 | 157 | /* 158 | * Create particles 159 | */ 160 | function createParticles () { 161 | for(var i = 0; i < config.particleCount; i++) { 162 | particles.push(new Particle()); 163 | } 164 | } 165 | 166 | 167 | /* 168 | * Distance calculator between two particles 169 | */ 170 | function calculateDistanceBetweenParticles (p1, p2) { 171 | 172 | var dist, 173 | dx = p1.x - p2.x, 174 | dy = p1.y - p2.y; 175 | 176 | dist = Math.sqrt(dx*dx + dy*dy); 177 | 178 | // Check whether distance is smaller than the min distance 179 | if(dist <= config.minimumAffectingDistance) { 180 | 181 | // set line opacity 182 | var opacity = 1 - dist/config.minimumAffectingDistance; 183 | 184 | // Draw connecting line 185 | ctx.beginPath(); 186 | ctx.strokeStyle = "rgba(255, 255, 255, " + opacity +")"; 187 | ctx.moveTo(p1.x, p1.y); 188 | ctx.lineTo(p2.x, p2.y); 189 | ctx.stroke(); 190 | ctx.closePath(); 191 | 192 | // Calculate particle acceleration 193 | var ax = dx / 2000, 194 | ay = dy / 2000; 195 | 196 | // Apply particle acceleration 197 | p1.vx -= ax; 198 | p1.vy -= ay; 199 | 200 | p2.vx += ax; 201 | p2.vy += ay; 202 | } 203 | } 204 | })(); 205 | -------------------------------------------------------------------------------- /_assets/css/form.css: -------------------------------------------------------------------------------- 1 | /* Defaults */ 2 | 3 | html { 4 | box-sizing: border-box; 5 | } 6 | 7 | *, 8 | :after, 9 | :before { 10 | box-sizing: inherit; 11 | } 12 | 13 | html, 14 | body { 15 | margin: 0; 16 | padding: 0; 17 | height: 100%; 18 | } 19 | 20 | html { 21 | font-family: Helvetica, sans-serif; 22 | font-size: 16px; 23 | } 24 | 25 | body { 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | } 30 | 31 | /* background */ 32 | 33 | .Background { 34 | background: linear-gradient(90deg, #7fa5d7 0%, #6ac9cf 100%); 35 | position: fixed; 36 | z-index: 1; 37 | left: 0; 38 | right: 0; 39 | top: 0; 40 | bottom: 0; 41 | } 42 | 43 | /* Wrap */ 44 | 45 | .Wrapper { 46 | height: 90%; 47 | width: 90%; 48 | display: flex; 49 | flex-direction: column; 50 | align-items: stretch; 51 | justify-content: center; 52 | position: relative; 53 | z-index: 2; 54 | } 55 | 56 | @media (min-width: 62.5rem) { 57 | .Wrapper { 58 | flex-direction: row; 59 | } 60 | } 61 | 62 | .Welcome, 63 | .Form { 64 | color: #333; 65 | text-align: center; 66 | } 67 | 68 | /* Welcome */ 69 | 70 | .Welcome { 71 | align-items: center; 72 | background: rgba(173, 216, 230, 0.9); 73 | display: flex; 74 | flex-direction: column; 75 | justify-content: center; 76 | padding: 1rem; 77 | } 78 | 79 | @media (min-width: 62.5rem) { 80 | .Welcome { 81 | flex: 1; 82 | } 83 | } 84 | 85 | .Welcome-title { 86 | font-size: 1.2rem; 87 | margin: 0.5rem 0; 88 | } 89 | 90 | @media (min-width: 62.5rem) { 91 | .Welcome-title { 92 | font-size: 4rem; 93 | margin: 0.5rem 0; 94 | } 95 | } 96 | 97 | .Welcome-text { 98 | margin: 0.5rem 1rem; 99 | font-size: 1rem; 100 | line-height: 1.5; 101 | } 102 | 103 | @media (min-width: 62.5rem) { 104 | .Welcome-text { 105 | font-size: 1.6rem; 106 | } 107 | } 108 | 109 | /* Form */ 110 | 111 | .Form { 112 | position: relative; 113 | background: rgba(255, 255, 255, 0.9); 114 | flex: 1; 115 | } 116 | 117 | .Form-content { 118 | height: 100%; 119 | overflow-y: scroll; 120 | padding: 1rem; 121 | } 122 | 123 | @media (min-width: 62.5rem) { 124 | .Form-row { 125 | width: 100%; 126 | } 127 | } 128 | 129 | .Form-label { 130 | display: block; 131 | font-size: 1rem; 132 | margin: 0.5rem 0; 133 | color: #999; 134 | } 135 | 136 | @media (min-width: 62.5rem) { 137 | .Form-label { 138 | font-size: 1.2rem; 139 | } 140 | } 141 | 142 | .Form-input, 143 | .Form-textarea { 144 | font-size: 1.4rem; 145 | background: rgba(255, 255, 255, 0.5); 146 | border: 0; 147 | border-bottom: 2px solid #6ac9cf; 148 | outline: none; 149 | margin-bottom: 2rem; 150 | max-width: 100%; 151 | padding: 0.5rem; 152 | text-align: center; 153 | transition: all 0.25s; 154 | } 155 | 156 | @media (min-width: 62.5rem) { 157 | .Form-input, 158 | .Form-textarea { 159 | font-size: 2rem; 160 | } 161 | } 162 | 163 | .Form-input:focus, 164 | .Form-textarea:focus { 165 | border-bottom-color: rgba(255, 204, 0, 1); 166 | background: rgba(255, 255, 255, 1); 167 | } 168 | 169 | .Form-selectWrap { 170 | background: rgba(255, 255, 255, 0.5); 171 | border-bottom: 2px solid #6ac9cf; 172 | display: block; 173 | margin-bottom: 3rem; 174 | position: relative; 175 | width: 100%; 176 | } 177 | 178 | .Form-select { 179 | border-radius: 0; 180 | box-sizing: border-box; 181 | font-size: 1.6rem; 182 | margin: 0; 183 | outline: none; 184 | padding: 0.5rem; 185 | width: 100%; 186 | } 187 | 188 | .Form-selectWrap::after { 189 | color: #000; 190 | content: "\25bc"; 191 | display: none; 192 | font-size: 12px; 193 | pointer-events: none; 194 | position: absolute; 195 | right: 1em; 196 | top: 50%; 197 | transform: translateY(-50%); 198 | z-index: 2; 199 | } 200 | 201 | @supports (-webkit-appearance: none) or (appearance: none) or 202 | ((-moz-appearance: none) and (mask-type: alpha)) { 203 | .Form-selectWrap::after { 204 | display: block; 205 | } 206 | 207 | .Form-select { 208 | -webkit-appearance: none; 209 | -moz-appearance: none; 210 | appearance: none; 211 | background: none; 212 | border: 1px solid transparent; 213 | padding-right: 2em; 214 | } 215 | } 216 | 217 | .Form-submit { 218 | background: rgba(255, 204, 0, 1); 219 | -webkit-appearance: none; 220 | border: none; 221 | display: block; 222 | margin-bottom: 2rem; 223 | width: 100%; 224 | padding: 1rem 3rem; 225 | font-size: 1.4rem; 226 | cursor: pointer; 227 | transition: all 0.5s; 228 | } 229 | 230 | @media (min-width: 62.5rem) { 231 | .Form-submit { 232 | font-size: 1.2rem; 233 | padding: 1.5rem 6rem; 234 | } 235 | } 236 | 237 | .Form-submit:disabled { 238 | background: rgba(255, 204, 0, 0.3); 239 | pointer-events: none; 240 | } 241 | 242 | .Form-submit:focus, 243 | .Form-submit:active { 244 | outline: none; 245 | } 246 | 247 | .Form-cancel { 248 | background: rgba(255, 255, 255, 0.8); 249 | -webkit-appearance: none; 250 | border: none; 251 | margin-bottom: 2rem; 252 | padding: 1rem 3rem; 253 | font-size: 0.8rem; 254 | } 255 | 256 | .Form-complete { 257 | position: absolute; 258 | display: flex; 259 | flex-direction: column; 260 | align-items: center; 261 | justify-content: center; 262 | left: 0; 263 | right: 0; 264 | top: 0; 265 | bottom: 0; 266 | background: rgba(255, 255, 255, 0.95); 267 | z-index: 2; 268 | opacity: 0; 269 | visibility: hidden; 270 | transition: all 0.5s; 271 | } 272 | 273 | .Form.is-complete .Form-complete { 274 | opacity: 1; 275 | top: 0; 276 | visibility: visible; 277 | } 278 | 279 | .Form-completeTitle { 280 | font-size: 3rem; 281 | margin-bottom: 1rem; 282 | } 283 | 284 | .Form-completeText { 285 | font-size: 1rem; 286 | } -------------------------------------------------------------------------------- /_assets/js/map.js: -------------------------------------------------------------------------------- 1 | (function(config) { 2 | 3 | var map; 4 | var tiles; 5 | 6 | var eventCount = 0; 7 | var timerEl; 8 | var statEventsEl; 9 | var logEl; 10 | var toggleEl; 11 | var contentEl; 12 | 13 | var icons = {}; 14 | 15 | function init() { 16 | findEls(); 17 | createMap(); 18 | createIcons(); 19 | createToggle(); 20 | loadData(); 21 | } 22 | 23 | function findEls() { 24 | timerEl = document.querySelector('.timer'); 25 | statEventsEl = document.querySelector('.stat-events'); 26 | logEl = document.querySelector('.log'); 27 | toggleEl = document.querySelector('.table-toggle'); 28 | contentEl = document.querySelector('.content'); 29 | } 30 | 31 | function createMap() { 32 | map = L.map('map', {}).setView([config.map.lat, config.map.lon], config.map.zoom); 33 | map.scrollWheelZoom.disable(); 34 | 35 | tiles = L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', { 36 | attribution: '© OpenStreetMap contributors', 37 | }).addTo(map); 38 | } 39 | 40 | function createIcons() { 41 | var types = { 42 | 'Astronomy': 'blue', 43 | 'Other event': 'green', 44 | 'Other club': 'orange', 45 | 'Curiosity Sussex': 'violet', 46 | }; 47 | 48 | for (var [topic, colour] of Object.entries(types)) { 49 | icons[topic] = new L.Icon({ 50 | iconUrl: '../_assets/img/marker-icon-2x-' + colour + '.png', 51 | shadowUrl: '../_assets/img/marker-shadow.png', 52 | iconSize: [25, 41], 53 | iconAnchor: [12, 41], 54 | popupAnchor: [1, -34], 55 | shadowSize: [41, 41] 56 | }); 57 | } 58 | } 59 | 60 | function createToggle() { 61 | toggleEl.addEventListener("click", toggleTable); 62 | } 63 | 64 | function toggleTable() { 65 | contentEl.classList.toggle('show-table'); 66 | var tableToggleStateEl = document.querySelector(".table-toggle-state"); 67 | if (tableToggleStateEl.textContent === "Show") { 68 | tableToggleStateEl.textContent = "Hide"; 69 | } else { 70 | tableToggleStateEl.textContent = "Show"; 71 | } 72 | } 73 | 74 | function loadData() { 75 | timerEl.classList.remove('is-waiting'); 76 | 77 | var url = 'https://sheets.googleapis.com/v4/spreadsheets/' + config.spreadsheet.id + '/values/' + config.spreadsheet.sheet + '!' + config.spreadsheet.range + '?key=' + config.spreadsheet.apiKey; 78 | 79 | request = new XMLHttpRequest(); 80 | request.open('GET', url, true); 81 | request.send(); 82 | request.onreadystatechange = updateData; 83 | } 84 | 85 | function updateData() { 86 | if (this.readyState === 4) { 87 | if (this.status === 200 || this.status === 0) { 88 | try { 89 | var response = JSON.parse(this.response); 90 | if (response.values) { 91 | 92 | // extract just the new responses to update the map 93 | if (response.values.length > eventCount) { 94 | var newEvents = response.values.slice(eventCount); 95 | eventCount = response.values.length; 96 | updateMap(newEvents); 97 | } 98 | } 99 | } catch (e) { 100 | console.log(e); 101 | } 102 | } 103 | 104 | // update every 30 seconds 105 | setTimeout(loadData, 1000 * 30); 106 | timerEl.classList.add('is-waiting'); 107 | } 108 | } 109 | 110 | function updateMap(newEvents) { 111 | newEvents.forEach(function(newEvent) { 112 | 113 | var data = { 114 | name: newEvent[1], 115 | url: newEvent[2], 116 | type: newEvent[3], 117 | date: new Date(newEvent[4]), 118 | startTime: newEvent[5], 119 | endTime: newEvent[6], 120 | address: newEvent[7], 121 | postcode: newEvent[8], 122 | lat: newEvent[9], 123 | lon: newEvent[10], 124 | visible: newEvent[11], 125 | }; 126 | 127 | // check the event is visible 128 | if (data.visible !== "TRUE") return false; 129 | 130 | // check the event is in the future 131 | var today = new Date().setHours(0,0,0,0); 132 | if (data.date.getTime() < today) return false; 133 | 134 | addMarker(data); 135 | addTableRow(data); 136 | logEvent(newEvent); 137 | }); 138 | } 139 | 140 | function addMarker(data) { 141 | 142 | // set icon based on event type 143 | var icon = icons[data.type]; 144 | 145 | // add marker to map 146 | var marker = L.marker([data.lat, data.lon], {icon: icon}).addTo(map); 147 | 148 | // set popup content 149 | var popupContent = document.createElement("div"); 150 | popupContent.classList.add("popup-content"); 151 | 152 | var popupLink = document.createElement("a"); 153 | popupLink.href = data.url; 154 | popupLink.target = "_blank"; 155 | popupLink.classList.add("popup-link"); 156 | popupContent.appendChild(popupLink); 157 | 158 | var popupName = document.createElement("p"); 159 | popupName.textContent = data.name; 160 | popupName.classList.add("popup-name"); 161 | popupLink.appendChild(popupName); 162 | 163 | var popupDate = document.createElement("p"); 164 | popupDate.textContent = data.date.toDateString() + " (" + data.startTime + "-" + data.endTime + ")"; 165 | popupDate.classList.add("popup-date"); 166 | popupContent.appendChild(popupDate); 167 | 168 | var popupAddress = document.createElement("p"); 169 | popupAddress.textContent = data.address + ", " + data.postcode; 170 | popupAddress.classList.add("popup-address"); 171 | popupContent.appendChild(popupAddress); 172 | 173 | marker.bindPopup(popupContent); 174 | } 175 | 176 | function addTableRow(data) { 177 | var tableRow = document.createElement("tr"); 178 | var tableBody = document.querySelector(".event-table-list"); 179 | tableBody.appendChild(tableRow); 180 | 181 | var tableName = document.createElement("td"); 182 | tableRow.appendChild(tableName); 183 | 184 | var tableLink = document.createElement("a"); 185 | tableLink.href = data.url; 186 | tableLink.target = "_blank"; 187 | tableLink.textContent = data.name; 188 | tableLink.classList.add("table-link"); 189 | tableName.appendChild(tableLink); 190 | 191 | var tableType = document.createElement("td"); 192 | tableType.textContent = data.type; 193 | tableType.classList.add("table-type"); 194 | tableRow.appendChild(tableType); 195 | 196 | var tableDate = document.createElement("td"); 197 | tableDate.textContent = data.date.toDateString() + " (" + data.startTime + "-" + data.endTime + ")"; 198 | tableDate.classList.add("table-date"); 199 | tableRow.appendChild(tableDate); 200 | 201 | var tableAddress = document.createElement("td"); 202 | tableAddress.textContent = data.address + ", " + data.postcode; 203 | tableAddress.classList.add("table-address"); 204 | tableRow.appendChild(tableAddress); 205 | } 206 | 207 | function logEvent(newEvent) { 208 | var eventCount = parseInt(statEventsEl.textContent, 10); 209 | statEventsEl.textContent = eventCount + 1; 210 | 211 | // var logItemEl = document.createElement("li"); 212 | // var logItemText = document.createTextNode(newEvent[1] + " added"); 213 | // logItemEl.appendChild(logItemText); 214 | // logEl.appendChild(logItemEl); 215 | 216 | // setTimeout(emptyLog, 1000 * 10); 217 | } 218 | 219 | function emptyLog() { 220 | while (logEl.firstChild) { 221 | logEl.removeChild(logEl.firstChild); 222 | } 223 | } 224 | 225 | init(); 226 | })(window.CONFIG); 227 | --------------------------------------------------------------------------------