├── .DS_Store ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── assets ├── brunch-config.js ├── css │ ├── _animations.css │ ├── _custom.css │ ├── _range.css │ ├── _transitions.css │ ├── _variables.css │ └── app.css ├── elm-package.json ├── elm │ ├── App.elm │ ├── Config.elm │ ├── Data │ │ ├── Dates.elm │ │ ├── Events.elm │ │ ├── Location │ │ │ ├── Postcode.elm │ │ │ └── Radius.elm │ │ ├── Maps.elm │ │ ├── Navigation.elm │ │ └── Ports.elm │ ├── Helpers │ │ ├── Delay.elm │ │ ├── Html.elm │ │ ├── Style.elm │ │ └── Window.elm │ ├── Request │ │ ├── CustomEvents.elm │ │ ├── MeetupEvents.elm │ │ └── Postcode.elm │ ├── State.elm │ ├── Types.elm │ ├── View.elm │ └── Views │ │ ├── Dates.elm │ │ ├── Events.elm │ │ ├── Layout.elm │ │ ├── Location.elm │ │ ├── Map.elm │ │ └── Navigation.elm ├── js │ ├── app.js │ ├── elm-embed.js │ ├── gmap.js │ └── interop.js ├── package-lock.json ├── package.json └── static │ ├── favicon.ico │ ├── images │ ├── calendar-white.svg │ ├── calendar.svg │ ├── chevron.svg │ ├── clock.svg │ ├── crosshair-white.svg │ ├── crosshair.svg │ ├── group.svg │ ├── plus.png │ ├── tech-for-good-summer.png │ └── tech-for-good.png │ └── robots.txt ├── compile_static.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib └── tech_for_good_near_you │ ├── accounts │ ├── accounts.ex │ └── admin.ex │ ├── application.ex │ ├── meet_ups │ ├── event.ex │ └── meet_ups.ex │ ├── repo.ex │ └── web │ ├── controllers │ ├── elm_controller.ex │ ├── event_controller.ex │ ├── meetup_controller.ex │ └── session_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── plugs │ └── auth.ex │ ├── requests │ └── lat_lon.ex │ ├── router.ex │ ├── templates │ ├── elm │ │ └── index.html.eex │ ├── event │ │ ├── confirmation.html.eex │ │ ├── edit.html.eex │ │ ├── form.html.eex │ │ ├── index.html.eex │ │ ├── new.html.eex │ │ ├── show.html.eex │ │ └── user_event.html.eex │ ├── layout │ │ ├── admin.html.eex │ │ └── app.html.eex │ └── session │ │ └── new.html.eex │ ├── views │ ├── elm_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ ├── event_view.ex │ ├── layout_view.ex │ └── session_view.ex │ └── web.ex ├── mix.exs ├── mix.lock ├── phoenix_static_buildpack.config ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20170620103715_create_meet_ups_event.exs │ ├── 20170620113852_add_url_column_to_events.exs │ ├── 20170620150507_add_lat_lon_to_events.exs │ ├── 20170623112555_create_accounts_admin.exs │ └── 20170704091944_add_approved_column_to_events.exs │ └── seeds.exs └── test ├── elm ├── Main.elm ├── Radius.elm └── elm-package.json ├── support ├── channel_case.ex ├── conn_case.ex └── data_case.ex ├── tech_for_good_near_you ├── meet_ups │ └── meet_ups_test.exs └── web │ ├── controllers │ ├── elm_controller_test.exs │ └── event_controller_test.exs │ └── views │ ├── elm_view_test.exs │ ├── error_view_test.exs │ └── layout_view_test.exs └── test_helper.exs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechforgoodCAST/tech-for-good-near-you/80e4d036cd866ba0da92969e2838dc8e8dfe5fe9/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs 28 | 29 | elm-stuff 30 | /assets/js/elm.js 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Centre for the Acceleration of Social Technology (CAST) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: MIX_ENV=prod mix phx.server 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tech for good near you 2 | 3 | ## What 4 | 5 | An app to help users find `Tech for good` events. Events will be filterable by date, area of interest and location. They will be displayed on a map along with the user's location. 6 | 7 | ## Why 8 | 9 | Currently `Tech for good` events are publicised in a range of mediums such as Twitter, MeetUp and Eventbrite. This makes it hard for interested people to get informed about all the events going on. 10 | 11 | ## How 12 | 13 | Event data is pulled in from the Meetup API and a collection of user submitted events. The app combines all the event data and displays events in a clean, intuitive interface. 14 | 15 | Users can enter their postcode or get their current location, select which date range they'd like to see events from and then see a list of all the events near them plus a map showing where these events are. 16 | 17 | The app is built with: 18 | 19 | * Elm - (frontend) 20 | * Tachyons - (frontend) 21 | * Elixir / Phoenix - (backend) 22 | 23 | 24 | ## Submit your event 25 | 26 | visit https://tech-for-good-near-you.herokuapp.com/user-event/new and add your event, our moderators will look over the events and approve them to be displayed. 27 | 28 | 29 | ### Icon Credits 30 | 31 | credits for icons used from [The Noun Project](https://thenounproject.com/) can be found in the [wiki](https://github.com/TechforgoodCAST/tech-for-good-near-you/wiki/Icon-credits) 32 | -------------------------------------------------------------------------------- /assets/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: 'js/app.js' 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // http://brunch.io/docs/config#-files- 9 | // joinTo: { 10 | // "js/app.js": /^js/, 11 | // "js/vendor.js": /^(?!js)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // order: { 16 | // before: [ 17 | // "vendor/js/jquery-2.1.1.js", 18 | // "vendor/js/bootstrap.min.js" 19 | // ] 20 | // } 21 | }, 22 | stylesheets: { 23 | joinTo: 'css/app.css', 24 | order: { 25 | after: ['css/app.css'] 26 | } 27 | }, 28 | templates: { 29 | joinTo: 'js/app.js' 30 | } 31 | }, 32 | 33 | conventions: { 34 | // This option sets where we should place non-css and non-js assets in. 35 | // By default, we set this to "/assets/static". Files in this directory 36 | // will be copied to `paths.public`, which is "priv/static" by default. 37 | assets: /^(static)/ 38 | }, 39 | 40 | // Phoenix paths configuration 41 | paths: { 42 | // Dependencies and current project directories to watch 43 | watched: ['static', 'css', 'js', 'vendor', 'elm'], 44 | // Where to compile files to 45 | public: '../priv/static' 46 | }, 47 | 48 | // Configure your plugins 49 | plugins: { 50 | babel: { 51 | // Do not use ES6 compiler in vendor code 52 | ignore: [/vendor/, /elm\.js/] 53 | }, 54 | postcss: { 55 | processors: [ 56 | require('autoprefixer')(), 57 | require('postcss-import')(), 58 | require('postcss-custom-media')(), 59 | require('postcss-custom-properties')() 60 | ] 61 | }, 62 | elmBrunch: { 63 | mainModules: ['elm/App.elm'], 64 | outputFile: 'elm.js', 65 | outputFolder: 'js', 66 | makeParameters: ['--debug'] 67 | } 68 | }, 69 | 70 | modules: { 71 | autoRequire: { 72 | 'js/app.js': ['js/app'] 73 | } 74 | }, 75 | 76 | npm: { 77 | enabled: true 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /assets/css/_animations.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { transform: rotateZ(0deg); } 3 | 100% { transform: rotateZ(360deg); } 4 | } 5 | 6 | @keyframes fade-in { 7 | 0% { opacity: 0; } 8 | 100% { opacity: 1; } 9 | } 10 | 11 | .spin { 12 | animation: spin 8s linear infinite; 13 | } 14 | 15 | .fade-in { 16 | animation: fade-in 1s ease; 17 | animation-fill-mode: forwards; 18 | } 19 | 20 | .a-3 { 21 | animation-duration: 0.3s!important; 22 | } 23 | -------------------------------------------------------------------------------- /assets/css/_custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: var(--sans-serif); 4 | } 5 | 6 | .pointer { 7 | cursor: pointer; 8 | } 9 | 10 | .no-select { 11 | user-select: none; 12 | } 13 | 14 | .disabled { 15 | pointer-events: none; 16 | } 17 | 18 | .mt--4 { 19 | margin-top: -var(--spacing-large); 20 | } 21 | 22 | .vh-60 { 23 | height: 60vh; 24 | } 25 | 26 | .smooth-scroll { 27 | -webkit-overflow-scrolling: touch; 28 | } 29 | 30 | .placeholder-white::placeholder { 31 | color: white; 32 | } 33 | 34 | @media (--breakpoint-not-small) { 35 | .bg-green-ns { 36 | background-color: var(--green); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/css/_range.css: -------------------------------------------------------------------------------- 1 | /* reset range */ 2 | 3 | input[type=range] { 4 | -webkit-appearance: none; 5 | width: 100%; 6 | background: transparent; 7 | } 8 | 9 | input[type=range]::-webkit-slider-thumb { 10 | -webkit-appearance: none; 11 | } 12 | 13 | input[type=range]:focus { 14 | outline: none; 15 | } 16 | 17 | input[type=range]::-ms-track { 18 | width: 100%; 19 | cursor: pointer; 20 | background: transparent; 21 | border-color: transparent; 22 | color: transparent; 23 | } 24 | 25 | 26 | /* range thumb */ 27 | 28 | input[type=range]::-webkit-slider-thumb { 29 | -webkit-appearance: none; 30 | height: 15px; 31 | width: 15px; 32 | border-radius: 100%; 33 | background: #ffffff; 34 | cursor: pointer; 35 | margin-top: -7px; 36 | } 37 | 38 | 39 | input[type=range]::-moz-range-thumb { 40 | height: 15px; 41 | width: 15px; 42 | border-radius: 100%; 43 | background: #ffffff; 44 | cursor: pointer; 45 | } 46 | 47 | 48 | input[type=range]::-ms-thumb { 49 | height: 15px; 50 | width: 15px; 51 | border-radius: 100%; 52 | background: #ffffff; 53 | cursor: pointer; 54 | } 55 | 56 | 57 | /* range track */ 58 | 59 | input[type=range]::-webkit-slider-runnable-track { 60 | width: 100%; 61 | height: 10px; 62 | cursor: pointer; 63 | background: transparent; 64 | } 65 | 66 | input[type=range]::-moz-range-track { 67 | width: 100%; 68 | height: 10px; 69 | cursor: pointer; 70 | } 71 | 72 | input[type=range]::-ms-track { 73 | width: 100%; 74 | height: 10px; 75 | cursor: pointer; 76 | border-color: transparent; 77 | color: transparent; 78 | } 79 | input[type=range]::-ms-fill-lower { 80 | background: transparent; 81 | } 82 | 83 | input[type=range]:focus::-ms-fill-lower { 84 | background: transparent; 85 | } 86 | 87 | input[type=range]::-ms-fill-upper { 88 | background: transparent; 89 | } 90 | 91 | input[type=range]:focus::-ms-fill-upper { 92 | background: transparent; 93 | } 94 | -------------------------------------------------------------------------------- /assets/css/_transitions.css: -------------------------------------------------------------------------------- 1 | .t-500ms { 2 | transition-duration: 0.5s!important; 3 | } 4 | 5 | .t-delay-500ms { 6 | transition-delay: 0.5s!important; 7 | } 8 | 9 | .all { 10 | transition-property: all!important; 11 | } 12 | 13 | .t-bg-color { 14 | transition-property: background-color!important; 15 | } 16 | 17 | .ease { 18 | transition-timing-function: ease!important; 19 | } 20 | -------------------------------------------------------------------------------- /assets/css/_variables.css: -------------------------------------------------------------------------------- 1 | @custom-media --breakpoint-not-small screen and (min-width: 30em); 2 | @custom-media --breakpoint-medium screen and (min-width: 45em); 3 | @custom-media --breakpoint-large screen and (min-width: 60em); 4 | 5 | :root { 6 | 7 | --sans-serif: lato, 'helvetica neue', helvetica, arial; 8 | --serif: georgia, serif; 9 | --code: consolas, monaco, monospace; 10 | 11 | --font-size-headline: 6rem; 12 | --font-size-subheadline: 5rem; 13 | --font-size-1: 3rem; 14 | --font-size-2: 2.25rem; 15 | --font-size-3: 1.3rem; 16 | --font-size-4: 1.25rem; 17 | --font-size-5: 1rem; 18 | --font-size-6: .8rem; 19 | 20 | --letter-spacing-tight:-.05em; 21 | --letter-spacing-1:.1em; 22 | --letter-spacing-2:.25em; 23 | 24 | --line-height-solid: 1; 25 | --line-height-title: 1.25; 26 | --line-height-copy: 1.5; 27 | 28 | --measure: 30em; 29 | --measure-narrow: 20em; 30 | --measure-wide: 34em; 31 | 32 | --spacing-none: 0; 33 | --spacing-extra-small: .25rem; 34 | --spacing-small: .5rem; 35 | --spacing-medium: 1rem; 36 | --spacing-large: 2rem; 37 | --spacing-extra-large: 4rem; 38 | --spacing-extra-extra-large: 8rem; 39 | --spacing-extra-extra-extra-large: 16rem; 40 | 41 | --height-1: 1rem; 42 | --height-2: 2rem; 43 | --height-3: 4rem; 44 | --height-4: 6rem; 45 | --height-5: 8rem; 46 | 47 | --width-1: 1rem; 48 | --width-2: 2rem; 49 | --width-3: 4rem; 50 | --width-4: 6rem; 51 | --width-5: 10rem; 52 | 53 | --max-width-1: 1rem; 54 | --max-width-2: 2rem; 55 | --max-width-3: 4rem; 56 | --max-width-4: 8rem; 57 | --max-width-5: 16rem; 58 | --max-width-6: 32rem; 59 | --max-width-7: 48rem; 60 | --max-width-8: 64rem; 61 | --max-width-9: 96rem; 62 | 63 | --border-radius-none: 0; 64 | --border-radius-1: .125rem; 65 | --border-radius-2: .25rem; 66 | --border-radius-3: .5rem; 67 | --border-radius-4: 1rem; 68 | --border-radius-circle: 100%; 69 | --border-radius-pill: 9999px; 70 | 71 | --border-width-none: 0; 72 | --border-width-1: .125rem; 73 | --border-width-2: .25rem; 74 | --border-width-3: .5rem; 75 | --border-width-4: 1rem; 76 | --border-width-5: 2rem; 77 | 78 | --box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 ); 79 | --box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 ); 80 | --box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 ); 81 | --box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 ); 82 | --box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 ); 83 | 84 | --black: #000; 85 | --near-black: #111; 86 | --dark-gray:#333; 87 | --mid-gray:#555; 88 | --gray: #777; 89 | --silver: #999; 90 | --light-silver: #aaa; 91 | --moon-gray: #ccc; 92 | --light-gray: #eee; 93 | --near-white: #f4f4f4; 94 | --white: #fff; 95 | 96 | --transparent:transparent; 97 | 98 | --black-90: rgba(0,0,0,.9); 99 | --black-80: rgba(0,0,0,.8); 100 | --black-70: rgba(0,0,0,.7); 101 | --black-60: rgba(0,0,0,.6); 102 | --black-50: rgba(0,0,0,.5); 103 | --black-40: rgba(0,0,0,.4); 104 | --black-30: rgba(0,0,0,.3); 105 | --black-20: rgba(0,0,0,.2); 106 | --black-10: rgba(0,0,0,.1); 107 | --black-05: rgba(0,0,0,.05); 108 | --black-025: rgba(0,0,0,.025); 109 | --black-0125: rgba(0,0,0,.0125); 110 | 111 | --white-90: rgba(255,255,255,.9); 112 | --white-80: rgba(255,255,255,.8); 113 | --white-70: rgba(255,255,255,.7); 114 | --white-60: rgba(255,255,255,.6); 115 | --white-50: rgba(255,255,255,.5); 116 | --white-40: rgba(255,255,255,.4); 117 | --white-30: rgba(255,255,255,.3); 118 | --white-20: rgba(255,255,255,.2); 119 | --white-10: rgba(255,255,255,.1); 120 | --white-05: rgba(255,255,255,.05); 121 | --white-025: rgba(255,255,255,.025); 122 | --white-0125: rgba(255,255,255,.0125); 123 | 124 | --dark-red: #e7040f; 125 | --red: #ff4136; 126 | --light-red: #f75c50; 127 | --orange: #ff6300; 128 | --gold: #ffb700; 129 | --yellow: #ffd700; 130 | --light-yellow: #fbf1a9; 131 | --purple: #5e2ca5; 132 | --light-purple: #a463f2; 133 | --dark-pink: #d5008f; 134 | --hot-pink: #ff41b4; 135 | --pink: #ff80cc; 136 | --light-pink: #ffa3d7; 137 | --dark-green: #00634c; 138 | --green: #19a974; 139 | --light-green: #C7FFDF; 140 | --navy: #001b44; 141 | --dark-blue: #00449e; 142 | --blue: #357edd; 143 | --light-blue: #96ccff; 144 | --lightest-blue: #cdecff; 145 | --washed-blue: #f6fffe; 146 | --washed-green: #E1F8EA; 147 | --washed-yellow: #fffceb; 148 | --washed-red: #ffdfdf; 149 | } 150 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | 3 | /* import tachyons modules */ 4 | 5 | @import 'tachyons-custom/src/_box-sizing'; 6 | @import 'tachyons-custom/src/_images'; 7 | @import 'tachyons-custom/src/_background-size'; 8 | @import 'tachyons-custom/src/_background-position'; 9 | @import 'tachyons-custom/src/_outlines'; 10 | @import 'tachyons-custom/src/_borders'; 11 | @import 'tachyons-custom/src/_border-colors'; 12 | @import 'tachyons-custom/src/_border-radius'; 13 | @import 'tachyons-custom/src/_border-style'; 14 | @import 'tachyons-custom/src/_border-widths'; 15 | @import 'tachyons-custom/src/_box-shadow'; 16 | @import 'tachyons-custom/src/_code'; 17 | @import 'tachyons-custom/src/_coordinates'; 18 | @import 'tachyons-custom/src/_clears'; 19 | @import 'tachyons-custom/src/_display'; 20 | @import 'tachyons-custom/src/_flexbox'; 21 | @import 'tachyons-custom/src/_floats'; 22 | @import 'tachyons-custom/src/_font-family'; 23 | @import 'tachyons-custom/src/_font-style'; 24 | @import 'tachyons-custom/src/_font-weight'; 25 | @import 'tachyons-custom/src/_forms'; 26 | @import 'tachyons-custom/src/_heights'; 27 | @import 'tachyons-custom/src/_letter-spacing'; 28 | @import 'tachyons-custom/src/_line-height'; 29 | @import 'tachyons-custom/src/_links'; 30 | @import 'tachyons-custom/src/_lists'; 31 | @import 'tachyons-custom/src/_max-widths'; 32 | @import 'tachyons-custom/src/_widths'; 33 | @import 'tachyons-custom/src/_overflow'; 34 | @import 'tachyons-custom/src/_position'; 35 | @import 'tachyons-custom/src/_opacity'; 36 | @import 'tachyons-custom/src/_skins'; 37 | @import 'tachyons-custom/src/_skins-pseudo'; 38 | @import 'tachyons-custom/src/_spacing'; 39 | @import 'tachyons-custom/src/_tables'; 40 | @import 'tachyons-custom/src/_text-decoration'; 41 | @import 'tachyons-custom/src/_text-align'; 42 | @import 'tachyons-custom/src/_text-transform'; 43 | @import 'tachyons-custom/src/_type-scale'; 44 | @import 'tachyons-custom/src/_typography'; 45 | @import 'tachyons-custom/src/_utilities'; 46 | @import 'tachyons-custom/src/_visibility'; 47 | @import 'tachyons-custom/src/_white-space'; 48 | @import 'tachyons-custom/src/_vertical-align'; 49 | @import 'tachyons-custom/src/_hovers'; 50 | @import 'tachyons-custom/src/_z-index'; 51 | 52 | /* custom variables */ 53 | @import './_variables'; 54 | @import './_transitions'; 55 | @import './_animations'; 56 | @import './_range'; 57 | @import './_custom'; 58 | -------------------------------------------------------------------------------- /assets/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "elm" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 12 | "andrewMacmurray/elm-delay": "1.0.0 <= v < 2.0.0", 13 | "ccapndave/elm-update-extra": "3.0.0 <= v < 4.0.0", 14 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 15 | "elm-lang/dom": "1.1.1 <= v < 2.0.0", 16 | "elm-lang/geolocation": "1.0.2 <= v < 2.0.0", 17 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 18 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 19 | "elm-lang/window": "1.0.1 <= v < 2.0.0", 20 | "justinmimbs/elm-date-extra": "2.0.3 <= v < 3.0.0", 21 | "krisajenkins/remotedata": "4.3.0 <= v < 5.0.0" 22 | }, 23 | "elm-version": "0.18.0 <= v < 0.19.0" 24 | } 25 | -------------------------------------------------------------------------------- /assets/elm/App.elm: -------------------------------------------------------------------------------- 1 | module App exposing (..) 2 | 3 | import Html exposing (program) 4 | import Types exposing (..) 5 | import State exposing (..) 6 | import View exposing (..) 7 | 8 | main : Program Never Model Msg 9 | main = 10 | program 11 | { init = init 12 | , update = update 13 | , view = view 14 | , subscriptions = subscriptions 15 | } 16 | -------------------------------------------------------------------------------- /assets/elm/Config.elm: -------------------------------------------------------------------------------- 1 | module Config exposing (..) 2 | 3 | 4 | mapId : String 5 | mapId = 6 | "t4g-google-map" 7 | 8 | 9 | eventsContainerId : String 10 | eventsContainerId = 11 | "events-container" 12 | 13 | 14 | mobileNav : { topHeight : Int, bottomHeight : Int } 15 | mobileNav = 16 | { topHeight = 60, bottomHeight = 50 } 17 | 18 | 19 | searchRadii : { national : Int, local : Int } 20 | searchRadii = 21 | { national = 400, local = 50 } 22 | -------------------------------------------------------------------------------- /assets/elm/Data/Dates.elm: -------------------------------------------------------------------------------- 1 | module Data.Dates exposing (..) 2 | 3 | import Date exposing (..) 4 | import Date.Extra exposing (..) 5 | import Types exposing (..) 6 | import Task 7 | import Time exposing (..) 8 | 9 | 10 | setCurrentDate : Time -> Model -> Model 11 | setCurrentDate today model = 12 | { model | today = Just <| fromTime today } 13 | 14 | 15 | handleSelectedDate : DateRange -> Model -> Model 16 | handleSelectedDate dateRange model = 17 | { model | selectedDate = dateRange } 18 | 19 | 20 | datesList : List DateRange 21 | datesList = 22 | [ Today 23 | , ThisWeek 24 | , ThisMonth 25 | , All 26 | ] 27 | 28 | 29 | getCurrentDate : Cmd Msg 30 | getCurrentDate = 31 | Task.perform CurrentDate Time.now 32 | 33 | 34 | filterByDate : Model -> List Event -> List Event 35 | filterByDate { selectedDate, today } = 36 | case selectedDate of 37 | Today -> 38 | List.filter <| isEventBefore Day today 39 | 40 | ThisWeek -> 41 | List.filter <| isEventBefore Week today 42 | 43 | ThisMonth -> 44 | List.filter <| isEventBefore Month today 45 | 46 | All -> 47 | allEvents 48 | 49 | 50 | allEvents : List Event -> List Event 51 | allEvents = 52 | identity 53 | 54 | 55 | isEventBefore : Interval -> Maybe Date -> Event -> Bool 56 | isEventBefore interval today event = 57 | Just (event.time) 58 | |> Maybe.map3 isBetween today (Maybe.map (Date.Extra.ceiling interval) today) 59 | |> Maybe.withDefault False 60 | 61 | 62 | noEventsInDateRange : DateRange -> String 63 | noEventsInDateRange dateRange = 64 | case dateRange of 65 | Today -> 66 | "No events near you today" 67 | 68 | ThisWeek -> 69 | "No events near you this week" 70 | 71 | ThisMonth -> 72 | "No events near you this month" 73 | 74 | All -> 75 | "No events found" 76 | 77 | 78 | dateRangeToString : DateRange -> String 79 | dateRangeToString dateRange = 80 | case dateRange of 81 | Today -> 82 | "Today" 83 | 84 | ThisWeek -> 85 | "This week" 86 | 87 | ThisMonth -> 88 | "This month" 89 | 90 | All -> 91 | "All events" 92 | 93 | 94 | displayDate : Date -> String 95 | displayDate date = 96 | Date.Extra.toFormattedString "MMMM d, h:mm a" date 97 | -------------------------------------------------------------------------------- /assets/elm/Data/Events.elm: -------------------------------------------------------------------------------- 1 | module Data.Events exposing (..) 2 | 3 | import Data.Dates exposing (filterByDate) 4 | import Data.Location.Radius exposing (..) 5 | import Date.Extra 6 | import RemoteData exposing (RemoteData(..), WebData, isFailure, isLoading) 7 | import Types exposing (..) 8 | 9 | 10 | handleFetchEvents : Model -> Model 11 | handleFetchEvents model = 12 | { model 13 | | meetupEvents = Loading 14 | , customEvents = Loading 15 | } 16 | 17 | 18 | addDistanceToEvents : Coords -> Model -> WebData (List Event) -> WebData (List Event) 19 | addDistanceToEvents fallbackLocation model events = 20 | let 21 | location = 22 | RemoteData.withDefault fallbackLocation model.userLocation 23 | in 24 | RemoteData.map (List.map <| calculateEventDistance location) events 25 | 26 | 27 | stillLoading : Model -> Bool 28 | stillLoading model = 29 | isLoading model.meetupEvents || isLoading model.customEvents 30 | 31 | 32 | bothEventRequestsFailed : Model -> Bool 33 | bothEventRequestsFailed model = 34 | isFailure model.meetupEvents && isFailure model.customEvents 35 | 36 | 37 | someEventsRetrieved : Model -> Bool 38 | someEventsRetrieved model = 39 | nonEmptyEvents model.meetupEvents || nonEmptyEvents model.customEvents 40 | 41 | 42 | nonEmptyEvents : WebData (List Event) -> Bool 43 | nonEmptyEvents events = 44 | events 45 | |> RemoteData.map (\evts -> List.length evts > 0) 46 | |> RemoteData.toMaybe 47 | |> Maybe.withDefault False 48 | 49 | 50 | filterEvents : Model -> WebData (List Event) 51 | filterEvents model = 52 | model 53 | |> allEvents 54 | |> RemoteData.map (filterByDate model >> filterByDistance model.searchRadius) 55 | 56 | 57 | numberVisibleEvents : Model -> Int 58 | numberVisibleEvents model = 59 | model 60 | |> filterEvents 61 | |> RemoteData.map List.length 62 | |> RemoteData.withDefault 0 63 | 64 | 65 | allEvents : Model -> WebData (List Event) 66 | allEvents model = 67 | RemoteData.append model.meetupEvents model.customEvents 68 | |> RemoteData.map (\( a, b ) -> a ++ b) 69 | |> RemoteData.map sortEventsByDate 70 | 71 | 72 | sortEventsByDate : List Event -> List Event 73 | sortEventsByDate = 74 | List.sortWith (\e1 e2 -> Date.Extra.compare e1.time e2.time) 75 | 76 | 77 | calculateEventDistance : Coords -> Event -> Event 78 | calculateEventDistance c1 event = 79 | { event | distance = latLngToMiles c1 (eventLatLng event) } 80 | 81 | 82 | eventLatLng : Event -> Coords 83 | eventLatLng event = 84 | Coords (eventLat event) (eventLng event) 85 | 86 | 87 | eventLat : Event -> Float 88 | eventLat event = 89 | if isPrivateEvent event then 90 | event.groupLat |> fallbackLat 91 | else 92 | event.lat |> fallbackLng 93 | 94 | 95 | eventLng : Event -> Float 96 | eventLng event = 97 | if isPrivateEvent event then 98 | event.groupLng |> fallbackLng 99 | else 100 | event.lng |> fallbackLat 101 | 102 | 103 | isPrivateEvent : Event -> Bool 104 | isPrivateEvent event = 105 | event.lat == Nothing || event.lng == Nothing 106 | 107 | 108 | fallbackLat : Maybe Float -> Float 109 | fallbackLat = 110 | Maybe.withDefault 51 111 | 112 | 113 | fallbackLng : Maybe Float -> Float 114 | fallbackLng = 115 | Maybe.withDefault 0 116 | -------------------------------------------------------------------------------- /assets/elm/Data/Location/Postcode.elm: -------------------------------------------------------------------------------- 1 | module Data.Location.Postcode exposing (..) 2 | 3 | import Config exposing (searchRadii) 4 | import Regex exposing (..) 5 | import RemoteData exposing (RemoteData(NotAsked)) 6 | import Types exposing (..) 7 | 8 | 9 | handleClearUserLocation : Model -> Model 10 | handleClearUserLocation model = 11 | { model 12 | | postcode = NotEntered 13 | , userLocation = NotAsked 14 | , searchRadius = searchRadii.national 15 | } 16 | 17 | 18 | handleUpdatePostcode : String -> Model -> Model 19 | handleUpdatePostcode postcode model = 20 | { model | postcode = validatePostcode postcode } 21 | 22 | 23 | validPostcode : Postcode -> Bool 24 | validPostcode postcode = 25 | case postcode of 26 | Valid _ -> 27 | True 28 | 29 | _ -> 30 | False 31 | 32 | 33 | validatePostcode : String -> Postcode 34 | validatePostcode postcode = 35 | if Regex.contains postcodeRegex postcode then 36 | Valid postcode 37 | else 38 | Invalid postcode 39 | 40 | 41 | postcodeRegex : Regex 42 | postcodeRegex = 43 | regex "^(([gG][iI][rR] {0,}0[aA]{2})|((([a-pr-uwyzA-PR-UWYZ][a-hk-yA-HK-Y]?[0-9][0-9]?)|(([a-pr-uwyzA-PR-UWYZ][0-9][a-hjkstuwA-HJKSTUW])|([a-pr-uwyzA-PR-UWYZ][a-hk-yA-HK-Y][0-9][abehmnprv-yABEHMNPRV-Y]))) {0,}[0-9][abd-hjlnp-uw-zABD-HJLNP-UW-Z]{2}))$" 44 | -------------------------------------------------------------------------------- /assets/elm/Data/Location/Radius.elm: -------------------------------------------------------------------------------- 1 | module Data.Location.Radius exposing (..) 2 | 3 | import Types exposing (..) 4 | 5 | 6 | filterByDistance : Int -> List Event -> List Event 7 | filterByDistance searchRadius = 8 | List.filter (withinSearchRadius searchRadius) 9 | 10 | 11 | withinSearchRadius : Int -> Event -> Bool 12 | withinSearchRadius searchRadius event = 13 | event.distance <= searchRadius 14 | 15 | 16 | latLngToMiles : Coords -> Coords -> Int 17 | latLngToMiles c1 c2 = 18 | let 19 | r = 20 | 3959 21 | 22 | distanceLat = 23 | degreesToRadians (c2.lat - c1.lat) 24 | 25 | distanceLng = 26 | degreesToRadians (c2.lng - c1.lng) 27 | 28 | a = 29 | ((sin (distanceLat / 2)) ^ 2) 30 | + cos (degreesToRadians c1.lat) 31 | * cos (degreesToRadians c2.lat) 32 | * ((sin (distanceLng / 2)) ^ 2) 33 | 34 | c = 35 | 2 * atan2 (sqrt a) (sqrt (1 - a)) 36 | in 37 | round (r * c) 38 | 39 | 40 | degreesToRadians : Float -> Float 41 | degreesToRadians deg = 42 | deg * (pi / 180) 43 | -------------------------------------------------------------------------------- /assets/elm/Data/Maps.elm: -------------------------------------------------------------------------------- 1 | module Data.Maps exposing (..) 2 | 3 | import Config 4 | import Data.Events exposing (eventLat, eventLng, filterEvents) 5 | import Data.Ports exposing (initMap, openBottomNav, updateMarkers) 6 | import Helpers.Delay exposing (googleMapDelay) 7 | import Helpers.Style exposing (isMobile) 8 | import RemoteData exposing (RemoteData) 9 | import Types exposing (..) 10 | 11 | 12 | updateMap : Cmd Msg 13 | updateMap = 14 | Cmd.batch 15 | [ googleMapDelay FitBounds 16 | , googleMapDelay RefreshMapSize 17 | , googleMapDelay FilteredMarkers 18 | ] 19 | 20 | 21 | refreshMapSize : Cmd Msg 22 | refreshMapSize = 23 | googleMapDelay RefreshMapSize 24 | 25 | 26 | initMapAtLondon : Model -> Cmd Msg 27 | initMapAtLondon model = 28 | initMap 29 | { marker = centerAtLondon 30 | , mapId = Config.mapId 31 | } 32 | 33 | 34 | updateFilteredMarkers : Model -> Cmd Msg 35 | updateFilteredMarkers model = 36 | model 37 | |> filterEvents 38 | |> RemoteData.withDefault [] 39 | |> extractMarkers 40 | |> updateMarkers 41 | 42 | 43 | extractMarkers : List Event -> List Marker 44 | extractMarkers = 45 | List.map makeMarker 46 | 47 | 48 | makeMarker : Event -> Marker 49 | makeMarker e = 50 | Marker e.url e.title (eventLat e) (eventLng e) 51 | 52 | 53 | centerAtLondon : Marker 54 | centerAtLondon = 55 | Marker "" "" 51.5062 0.1164 56 | 57 | 58 | londonCoords : Coords 59 | londonCoords = 60 | Coords 51.5062 0.1164 61 | -------------------------------------------------------------------------------- /assets/elm/Data/Navigation.elm: -------------------------------------------------------------------------------- 1 | module Data.Navigation exposing (..) 2 | 3 | import Helpers.Style exposing (isMobile) 4 | import Types exposing (..) 5 | 6 | 7 | handleResetMobileNav : Model -> Model 8 | handleResetMobileNav model = 9 | { model 10 | | bottomNavOpen = False 11 | , mobileDateOptionsVisible = False 12 | } 13 | 14 | 15 | handleToggleTopNavbar : Model -> Model 16 | handleToggleTopNavbar model = 17 | if isMobile model then 18 | { model | topNavOpen = not model.topNavOpen } 19 | else 20 | model 21 | -------------------------------------------------------------------------------- /assets/elm/Data/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Data.Ports 2 | exposing 3 | ( initMap 4 | , updateMarkers 5 | , updateUserLocation 6 | , clearUserLocation 7 | , centerMapOnUser 8 | , centerEvent 9 | , fitBounds 10 | , resizeMap 11 | , scrollToEvent 12 | , openBottomNav 13 | ) 14 | 15 | import Types exposing (..) 16 | 17 | 18 | port initMap : MapOptions -> Cmd msg 19 | 20 | 21 | port updateMarkers : List Marker -> Cmd msg 22 | 23 | 24 | port updateUserLocation : Coords -> Cmd msg 25 | 26 | 27 | clearUserLocation : Cmd msg 28 | clearUserLocation = 29 | clearUserLocation_ () 30 | 31 | 32 | port clearUserLocation_ : () -> Cmd msg 33 | 34 | 35 | centerMapOnUser : Cmd Msg 36 | centerMapOnUser = 37 | centerMapOnUser_ () 38 | 39 | 40 | port centerMapOnUser_ : () -> Cmd msg 41 | 42 | 43 | port centerEvent : Marker -> Cmd msg 44 | 45 | 46 | fitBounds : Cmd msg 47 | fitBounds = 48 | fitBounds_ () 49 | 50 | 51 | port fitBounds_ : () -> Cmd msg 52 | 53 | 54 | resizeMap : Cmd msg 55 | resizeMap = 56 | resizeMap_ () 57 | 58 | 59 | port resizeMap_ : () -> Cmd msg 60 | 61 | 62 | port scrollToEvent : (Float -> msg) -> Sub msg 63 | 64 | 65 | port openBottomNav : (Bool -> msg) -> Sub msg 66 | -------------------------------------------------------------------------------- /assets/elm/Helpers/Delay.elm: -------------------------------------------------------------------------------- 1 | module Helpers.Delay exposing (..) 2 | 3 | import Types exposing (..) 4 | import Delay 5 | 6 | 7 | googleMapDelay : Msg -> Cmd Msg 8 | googleMapDelay msg = 9 | Delay.after 50 msg 10 | -------------------------------------------------------------------------------- /assets/elm/Helpers/Html.elm: -------------------------------------------------------------------------------- 1 | module Helpers.Html exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Json.Encode exposing (string) 6 | import Json.Decode as Decode 7 | import Html.Events exposing (on, keyCode) 8 | 9 | 10 | responsiveImg : String -> Html msg 11 | responsiveImg imgSrc = 12 | img [ class "w-100", src imgSrc ] [] 13 | 14 | 15 | emptyProperty : Attribute msg 16 | emptyProperty = 17 | property "" <| string "" 18 | 19 | 20 | onEnter : msg -> Attribute msg 21 | onEnter msg = 22 | let 23 | isEnter code = 24 | if code == 13 then 25 | Decode.succeed msg 26 | else 27 | Decode.fail "" 28 | in 29 | on "keydown" (Decode.andThen isEnter keyCode) 30 | -------------------------------------------------------------------------------- /assets/elm/Helpers/Style.elm: -------------------------------------------------------------------------------- 1 | module Helpers.Style exposing (..) 2 | 3 | import Config exposing (mobileNav) 4 | import Html exposing (Attribute) 5 | import Html.Attributes exposing (class, style) 6 | import Types exposing (..) 7 | 8 | 9 | classes : List String -> Attribute msg 10 | classes = 11 | class << String.join " " 12 | 13 | 14 | styles : List (List Style) -> Attribute msg 15 | styles = 16 | style << List.concat 17 | 18 | 19 | mobileOnly : String 20 | mobileOnly = 21 | "db dn-ns" 22 | 23 | 24 | desktopOnly : String 25 | desktopOnly = 26 | "dn db-ns" 27 | 28 | 29 | anchorBottom : String 30 | anchorBottom = 31 | "absolute bottom-0 left-0" 32 | 33 | 34 | px : number -> String 35 | px n = 36 | (toString n) ++ "px" 37 | 38 | 39 | deg : number -> String 40 | deg n = 41 | (toString n) ++ "deg" 42 | 43 | 44 | transform : String -> Style 45 | transform x = 46 | ( "transform", x ) 47 | 48 | 49 | translateY : number -> String 50 | translateY y = 51 | "translateY(" ++ px y ++ ")" 52 | 53 | 54 | rotateZ : number -> String 55 | rotateZ angle = 56 | "rotateZ(" ++ deg angle ++ ")" 57 | 58 | 59 | ifDesktop : Style -> Model -> Style 60 | ifDesktop style model = 61 | if isDesktop model then 62 | style 63 | else 64 | emptyStyle 65 | 66 | 67 | ifMobile : Model -> Style -> Style 68 | ifMobile model style = 69 | if isMobile model then 70 | style 71 | else 72 | emptyStyle 73 | 74 | 75 | isDesktop : Model -> Bool 76 | isDesktop model = 77 | model.window.width >= 480 78 | 79 | 80 | isMobile : Model -> Bool 81 | isMobile model = 82 | isDesktop model |> not 83 | 84 | 85 | hideWhenShortScreen : Model -> String 86 | hideWhenShortScreen model = 87 | if shortScreen model then 88 | "dn" 89 | else 90 | "" 91 | 92 | 93 | shortScreen : Model -> Bool 94 | shortScreen model = 95 | model.window.height < 630 96 | 97 | 98 | percentScreenHeight : Int -> Model -> Style 99 | percentScreenHeight percent { window } = 100 | let 101 | height = 102 | window.height // 100 * percent 103 | in 104 | ( "height", px height ) 105 | 106 | 107 | mobileMaxHeight : Model -> Style 108 | mobileMaxHeight ({ window } as model) = 109 | ( "height", px <| window.height - mobileNav.topHeight ) 110 | |> ifMobile model 111 | 112 | 113 | mobileFullHeight : Model -> Style 114 | mobileFullHeight ({ window } as model) = 115 | ( "height", px <| window.height - mobileNav.topHeight - mobileNav.bottomHeight ) 116 | |> ifMobile model 117 | 118 | 119 | emptyStyle : Style 120 | emptyStyle = 121 | ( "", "" ) 122 | -------------------------------------------------------------------------------- /assets/elm/Helpers/Window.elm: -------------------------------------------------------------------------------- 1 | module Helpers.Window exposing (..) 2 | 3 | import Config exposing (mobileNav) 4 | import Data.Events exposing (someEventsRetrieved) 5 | import Dom.Scroll as Scroll 6 | import Helpers.Style exposing (isMobile) 7 | import Task 8 | import Types exposing (..) 9 | import Window 10 | 11 | 12 | handleScrollEventsToTop : Model -> Cmd Msg 13 | handleScrollEventsToTop model = 14 | if someEventsRetrieved model then 15 | scrollEventsToTop model 16 | else 17 | Cmd.none 18 | 19 | 20 | getWindowSize : Cmd Msg 21 | getWindowSize = 22 | Window.size |> Task.perform WindowSize 23 | 24 | 25 | scrollEventsToTop : Model -> Cmd Msg 26 | scrollEventsToTop model = 27 | Scroll.toTop Config.eventsContainerId 28 | |> Task.attempt Scroll 29 | 30 | 31 | scrollEventContainer : Float -> Model -> Cmd Msg 32 | scrollEventContainer offset model = 33 | Scroll.toY Config.eventsContainerId (calculateEventsOffset offset model) 34 | |> Task.attempt Scroll 35 | 36 | 37 | calculateEventsOffset : Float -> Model -> Float 38 | calculateEventsOffset offset model = 39 | if isMobile model then 40 | offset - toFloat (model.window.height // 2) - toFloat (mobileNav.bottomHeight // 2) 41 | else 42 | offset - toFloat (model.window.height // 2) 43 | -------------------------------------------------------------------------------- /assets/elm/Request/CustomEvents.elm: -------------------------------------------------------------------------------- 1 | module Request.CustomEvents exposing (..) 2 | 3 | import Data.Events exposing (addDistanceToEvents) 4 | import Data.Maps exposing (londonCoords) 5 | import Date exposing (..) 6 | import Http 7 | import Json.Decode as Json exposing (..) 8 | import Json.Decode.Pipeline exposing (..) 9 | import RemoteData exposing (WebData) 10 | import Types exposing (..) 11 | 12 | 13 | handleReceiveCustomEvents : WebData (List Event) -> Model -> Model 14 | handleReceiveCustomEvents customEvents model = 15 | { model | customEvents = addDistanceToEvents londonCoords model customEvents } 16 | 17 | 18 | getCustomEvents : Cmd Msg 19 | getCustomEvents = 20 | Http.get "api/custom-events" (field "data" (list decodeCustomEvent)) 21 | |> RemoteData.sendRequest 22 | |> Cmd.map ReceiveCustomEvents 23 | 24 | 25 | decodeCustomEvent : Decoder Event 26 | decodeCustomEvent = 27 | decode Event 28 | |> required "name" string 29 | |> required "url" string 30 | |> required "time" stringToDate 31 | |> optional "address" string "" 32 | |> optional "venue_name" string "" 33 | |> optional "latitude" (maybe float) Nothing 34 | |> optional "longitude" (maybe float) Nothing 35 | |> optional "group_lat" (maybe float) Nothing 36 | |> optional "goup_lng" (maybe float) Nothing 37 | |> optional "yes_rsvp_count" int 0 38 | |> optional "group_name" string "" 39 | |> hardcoded 0 40 | 41 | 42 | stringToDate : Decoder Date 43 | stringToDate = 44 | string 45 | |> Json.map Date.fromString 46 | |> Json.map (Result.withDefault <| fromTime 0) 47 | -------------------------------------------------------------------------------- /assets/elm/Request/MeetupEvents.elm: -------------------------------------------------------------------------------- 1 | module Request.MeetupEvents exposing (..) 2 | 3 | import Data.Events exposing (addDistanceToEvents) 4 | import Data.Maps exposing (londonCoords) 5 | import Date exposing (..) 6 | import Http 7 | import Json.Decode as Json exposing (..) 8 | import Json.Decode.Pipeline exposing (..) 9 | import RemoteData exposing (WebData) 10 | import Types exposing (..) 11 | 12 | 13 | handleReceiveMeetupEvents : WebData (List Event) -> Model -> Model 14 | handleReceiveMeetupEvents meetupEvents model = 15 | { model | meetupEvents = addDistanceToEvents londonCoords model meetupEvents } 16 | 17 | 18 | getMeetupEvents : Cmd Msg 19 | getMeetupEvents = 20 | Http.get "api/meetup-events" (list decodeEvent) 21 | |> RemoteData.sendRequest 22 | |> Cmd.map ReceiveMeetupEvents 23 | 24 | 25 | decodeEvent : Decoder Event 26 | decodeEvent = 27 | decode Event 28 | |> required "name" string 29 | |> required "event_url" string 30 | |> required "time" floatToDate 31 | |> optionalAt [ "venue", "address_1" ] string "" 32 | |> optionalAt [ "venue", "name" ] string "" 33 | |> optionalAt [ "venue", "lat" ] (maybe float) Nothing 34 | |> optionalAt [ "venue", "lon" ] (maybe float) Nothing 35 | |> optionalAt [ "group", "group_lat" ] (maybe float) Nothing 36 | |> optionalAt [ "group", "group_lon" ] (maybe float) Nothing 37 | |> required "yes_rsvp_count" int 38 | |> requiredAt [ "group", "urlname" ] urlToGroupTitle 39 | |> hardcoded 0 40 | 41 | 42 | floatToDate : Decoder Date 43 | floatToDate = 44 | float |> Json.map fromTime 45 | 46 | 47 | urlToGroupTitle : Decoder String 48 | urlToGroupTitle = 49 | string |> Json.map (String.split "-" >> String.join " ") 50 | -------------------------------------------------------------------------------- /assets/elm/Request/Postcode.elm: -------------------------------------------------------------------------------- 1 | module Request.Postcode exposing (..) 2 | 3 | import Http 4 | import Json.Decode exposing (..) 5 | import Json.Decode.Pipeline exposing (..) 6 | import Types exposing (..) 7 | import RemoteData exposing (RemoteData(..), WebData) 8 | 9 | 10 | handleRecievePostcodeLatLng : WebData Coords -> Model -> Model 11 | handleRecievePostcodeLatLng webData model = 12 | case webData of 13 | Success coords -> 14 | { model | userLocation = Success coords } 15 | 16 | _ -> 17 | { model | userLocation = webData } 18 | 19 | 20 | handleGetLatLngFromPostcode : Model -> Cmd Msg 21 | handleGetLatLngFromPostcode model = 22 | case model.postcode of 23 | NotEntered -> 24 | Cmd.none 25 | 26 | Invalid _ -> 27 | Cmd.none 28 | 29 | Valid postcode -> 30 | postcodeRequest postcode 31 | 32 | 33 | postcodeRequest : String -> Cmd Msg 34 | postcodeRequest postcode = 35 | Http.get (postcodeUrl postcode) (at [ "result" ] postcodeDecoder) 36 | |> RemoteData.sendRequest 37 | |> Cmd.map RecievePostcodeLatLng 38 | 39 | 40 | postcodeUrl : String -> String 41 | postcodeUrl postcode = 42 | "https://api.postcodes.io/postcodes/" ++ (String.filter ((/=) ' ') postcode) 43 | 44 | 45 | postcodeDecoder : Decoder Coords 46 | postcodeDecoder = 47 | decode Coords 48 | |> required "latitude" float 49 | |> required "longitude" float 50 | -------------------------------------------------------------------------------- /assets/elm/State.elm: -------------------------------------------------------------------------------- 1 | module State exposing (..) 2 | 3 | import Config exposing (searchRadii) 4 | import Data.Dates exposing (getCurrentDate, handleSelectedDate, setCurrentDate) 5 | import Data.Events exposing (..) 6 | import Data.Location.Postcode exposing (..) 7 | import Data.Maps exposing (..) 8 | import Data.Navigation exposing (handleResetMobileNav, handleToggleTopNavbar) 9 | import Data.Ports exposing (..) 10 | import Helpers.Window exposing (getWindowSize, handleScrollEventsToTop, scrollEventContainer) 11 | import RemoteData exposing (RemoteData(..)) 12 | import Request.CustomEvents exposing (getCustomEvents, handleReceiveCustomEvents) 13 | import Request.MeetupEvents exposing (getMeetupEvents, handleReceiveMeetupEvents) 14 | import Request.Postcode exposing (handleGetLatLngFromPostcode, handleRecievePostcodeLatLng) 15 | import Types exposing (..) 16 | import Update.Extra exposing (addCmd, andThen) 17 | import Window exposing (resizes) 18 | 19 | 20 | init : ( Model, Cmd Msg ) 21 | init = 22 | initialModel 23 | ! [ getCurrentDate 24 | , initMapAtLondon initialModel 25 | , getMeetupEvents 26 | , getCustomEvents 27 | , getWindowSize 28 | ] 29 | 30 | 31 | initialModel : Model 32 | initialModel = 33 | { postcode = NotEntered 34 | , selectedDate = All 35 | , today = Nothing 36 | , meetupEvents = Loading 37 | , customEvents = Loading 38 | , userLocation = NotAsked 39 | , searchRadius = searchRadii.national 40 | , topNavOpen = False 41 | , bottomNavOpen = False 42 | , mobileDateOptionsVisible = False 43 | , window = { width = 0, height = 0 } 44 | } 45 | 46 | 47 | update : Msg -> Model -> ( Model, Cmd Msg ) 48 | update msg model = 49 | case msg of 50 | UpdatePostcode postcode -> 51 | handleUpdatePostcode postcode model ! [] 52 | 53 | ClearUserLocation -> 54 | (handleClearUserLocation model ! [ clearUserLocation ]) 55 | |> andThen update FilteredMarkers 56 | 57 | SetDateRange date -> 58 | (handleSelectedDate date model ! []) 59 | |> andThen update FilteredMarkers 60 | |> addCmd (handleScrollEventsToTop model) 61 | 62 | CurrentDate date -> 63 | setCurrentDate date model ! [] 64 | 65 | ReceiveMeetupEvents events -> 66 | (handleReceiveMeetupEvents events model ! []) |> andThen update UpdateMap 67 | 68 | ReceiveCustomEvents events -> 69 | (handleReceiveCustomEvents events model ! []) |> andThen update UpdateMap 70 | 71 | FetchEvents -> 72 | handleFetchEvents model ! [ getMeetupEvents, getCustomEvents ] 73 | 74 | FetchEventsForPostcode -> 75 | { model | searchRadius = searchRadii.local } ! [ handleGetLatLngFromPostcode model ] 76 | 77 | RecievePostcodeLatLng (Success coords) -> 78 | (handleRecievePostcodeLatLng (Success coords) model ! [ updateUserLocation coords ]) 79 | |> andThen update FetchEvents 80 | 81 | RecievePostcodeLatLng remoteData -> 82 | handleRecievePostcodeLatLng remoteData model ! [] 83 | 84 | MobileDateVisible bool -> 85 | { model | mobileDateOptionsVisible = bool } ! [] 86 | 87 | UpdateMap -> 88 | model ! [ updateMap ] 89 | 90 | CenterEvent marker -> 91 | model ! [ centerEvent marker ] 92 | 93 | FitBounds -> 94 | model ! [ fitBounds ] 95 | 96 | ToggleTopNavbar -> 97 | handleToggleTopNavbar model ! [] 98 | 99 | BottomNavOpen bool -> 100 | { model | bottomNavOpen = bool } ! [ refreshMapSize ] 101 | 102 | ResetMobileNav -> 103 | handleResetMobileNav model ! [ refreshMapSize ] 104 | 105 | FilteredMarkers -> 106 | model ! [ updateFilteredMarkers model ] 107 | 108 | CenterMapOnUser -> 109 | model ! [ centerMapOnUser ] 110 | 111 | RefreshMapSize -> 112 | model ! [ resizeMap ] 113 | 114 | WindowSize size -> 115 | { model | window = size } ! [] 116 | 117 | ScrollToEvent offset -> 118 | model ! [ scrollEventContainer offset model ] 119 | 120 | Scroll _ -> 121 | model ! [] 122 | 123 | 124 | subscriptions : Model -> Sub Msg 125 | subscriptions model = 126 | Sub.batch 127 | [ resizes WindowSize 128 | , scrollToEvent ScrollToEvent 129 | ] 130 | -------------------------------------------------------------------------------- /assets/elm/Types.elm: -------------------------------------------------------------------------------- 1 | module Types exposing (..) 2 | 3 | import Date exposing (..) 4 | import Dom 5 | import RemoteData exposing (WebData) 6 | import Time exposing (..) 7 | import Window 8 | 9 | 10 | type alias Model = 11 | { postcode : Postcode 12 | , selectedDate : DateRange 13 | , today : Maybe Date 14 | , meetupEvents : WebData (List Event) 15 | , customEvents : WebData (List Event) 16 | , userLocation : WebData Coords 17 | , searchRadius : Int 18 | , topNavOpen : Bool 19 | , bottomNavOpen : Bool 20 | , mobileDateOptionsVisible : Bool 21 | , window : Window.Size 22 | } 23 | 24 | 25 | type alias Event = 26 | { title : String 27 | , url : String 28 | , time : Date 29 | , address : String 30 | , venueName : String 31 | , lat : Maybe Float 32 | , lng : Maybe Float 33 | , groupLat : Maybe Float 34 | , groupLng : Maybe Float 35 | , rsvpCount : Int 36 | , groupName : String 37 | , distance : Int 38 | } 39 | 40 | 41 | type Postcode 42 | = NotEntered 43 | | Invalid String 44 | | Valid String 45 | 46 | 47 | type DateRange 48 | = Today 49 | | ThisWeek 50 | | ThisMonth 51 | | All 52 | 53 | 54 | type alias Coords = 55 | { lat : Float 56 | , lng : Float 57 | } 58 | 59 | 60 | type alias MapOptions = 61 | { marker : Marker 62 | , mapId : String 63 | } 64 | 65 | 66 | type alias Marker = 67 | { url : String 68 | , title : String 69 | , lat : Float 70 | , lng : Float 71 | } 72 | 73 | 74 | type Msg 75 | = UpdatePostcode String 76 | | ClearUserLocation 77 | | SetDateRange DateRange 78 | | ReceiveMeetupEvents (WebData (List Event)) 79 | | ReceiveCustomEvents (WebData (List Event)) 80 | | CurrentDate Time 81 | | RecievePostcodeLatLng (WebData Coords) 82 | | CenterMapOnUser 83 | | CenterEvent Marker 84 | | FetchEvents 85 | | FetchEventsForPostcode 86 | | FitBounds 87 | | ToggleTopNavbar 88 | | MobileDateVisible Bool 89 | | BottomNavOpen Bool 90 | | UpdateMap 91 | | ResetMobileNav 92 | | FilteredMarkers 93 | | RefreshMapSize 94 | | WindowSize Window.Size 95 | | ScrollToEvent Float 96 | | Scroll (Result Dom.Error ()) 97 | 98 | 99 | type alias Style = 100 | ( String, String ) 101 | -------------------------------------------------------------------------------- /assets/elm/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (..) 2 | 3 | import Html exposing (..) 4 | import Types exposing (..) 5 | import Views.Events exposing (events) 6 | import Views.Layout exposing (layout, loadingScreen) 7 | import Views.Map exposing (renderMap) 8 | 9 | 10 | view : Model -> Html Msg 11 | view model = 12 | div [] 13 | [ loadingScreen model 14 | , renderMap model 15 | , layout model <| events model 16 | ] 17 | -------------------------------------------------------------------------------- /assets/elm/Views/Dates.elm: -------------------------------------------------------------------------------- 1 | module Views.Dates exposing (..) 2 | 3 | import Data.Dates exposing (dateRangeToString, datesList) 4 | import Helpers.Style exposing (px) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (onClick, onInput) 8 | import Types exposing (..) 9 | 10 | 11 | type DateButton 12 | = SideBar 13 | | BottomBar 14 | | FullPage 15 | 16 | 17 | dateBottomBarOptions : Model -> Html Msg 18 | dateBottomBarOptions model = 19 | div [] <| List.map (dateButton BottomBar model) datesList 20 | 21 | 22 | dateMainOptions : Model -> Html Msg 23 | dateMainOptions model = 24 | div [ class "tc mt4" ] <| List.map (dateButton FullPage model) datesList 25 | 26 | 27 | dateSideOptions : Model -> Html Msg 28 | dateSideOptions model = 29 | div [ class "mt3-ns", style [ ( "width", px 120 ) ] ] 30 | [ p [ class "white" ] [ text "events from:" ] 31 | , div [] (List.map (dateButton SideBar model) datesList) 32 | ] 33 | 34 | 35 | dateButton : DateButton -> Model -> DateRange -> Html Msg 36 | dateButton buttonType model date = 37 | let 38 | ( bodyClasses, offClasses, onClasses ) = 39 | buttonClasses buttonType 40 | in 41 | div 42 | [ class ("br2 ba pointer t-3 t-bg-color ease no-select " ++ bodyClasses) 43 | , classList 44 | [ ( offClasses, date == model.selectedDate ) 45 | , ( onClasses, date /= model.selectedDate ) 46 | ] 47 | , onClick (SetDateRange date) 48 | ] 49 | [ span [ class "f6 fw4" ] [ text (dateRangeToString date) ] ] 50 | 51 | 52 | buttonClasses : DateButton -> ( String, String, String ) 53 | buttonClasses buttonType = 54 | case buttonType of 55 | SideBar -> 56 | ( "b--white pv1 ph2 ma2 ml0", "bg-white green", "white" ) 57 | 58 | BottomBar -> 59 | ( "b--white dib ma1 ph1 pb1", "bg-white green", "white" ) 60 | 61 | FullPage -> 62 | ( "b--green pv2 ph5 ma2", "bg-green white", "green" ) 63 | -------------------------------------------------------------------------------- /assets/elm/Views/Events.elm: -------------------------------------------------------------------------------- 1 | module Views.Events exposing (..) 2 | 3 | import Config exposing (mobileNav) 4 | import Data.Dates exposing (..) 5 | import Data.Events exposing (..) 6 | import Data.Maps exposing (..) 7 | import Helpers.Html exposing (responsiveImg) 8 | import Helpers.Style exposing (..) 9 | import Html exposing (..) 10 | import Html.Attributes exposing (..) 11 | import Html.Events exposing (onClick) 12 | import RemoteData exposing (RemoteData) 13 | import Types exposing (..) 14 | import Views.Dates exposing (dateMainOptions) 15 | import Views.Layout exposing (desktopCredit) 16 | 17 | 18 | events : Model -> Html Msg 19 | events model = 20 | div 21 | [ style [ ( "margin-top", px <| mapMargin model ) ] 22 | , class "w-100 smooth-scroll" 23 | ] 24 | [ eventsResultsStates model 25 | ] 26 | 27 | 28 | eventsResultsStates : Model -> Html Msg 29 | eventsResultsStates model = 30 | if bothEventRequestsFailed model then 31 | eventsError model 32 | else if numberVisibleEvents model == 0 && not (stillLoading model) then 33 | selectOtherDates model 34 | else 35 | allEvents model 36 | 37 | 38 | eventsError : Model -> Html msg 39 | eventsError model = 40 | div [ class "mt4 red tc" ] 41 | [ p [] [ text "something went wrong fetching the events" ] 42 | , p [] [ text "try refreshing the page" ] 43 | ] 44 | 45 | 46 | selectOtherDates : Model -> Html Msg 47 | selectOtherDates model = 48 | div [ classes [ "green tc fade-in", desktopOnly ] ] 49 | [ p [ class "fade-in f4 mt5-ns mt4" ] [ text <| noEventsInDateRange model.selectedDate ] 50 | , p [ class "f6" ] [ text "Choose another date" ] 51 | , div [ class "center mw5" ] [ dateMainOptions model ] 52 | , div [ class "mt5" ] [ desktopCredit ] 53 | ] 54 | 55 | 56 | allEvents : Model -> Html Msg 57 | allEvents model = 58 | div 59 | [ classes [ "ph4-ns w-100 overflow-y-scroll smooth-scroll" ] 60 | , style [ ( "height", px <| mapMargin model ) ] 61 | , id Config.eventsContainerId 62 | ] 63 | <| 64 | renderEvents model 65 | 66 | 67 | renderEvents : Model -> List (Html Msg) 68 | renderEvents model = 69 | model 70 | |> filterEvents 71 | |> RemoteData.withDefault [] 72 | |> List.map renderEvent 73 | 74 | 75 | mapMargin : Model -> Int 76 | mapMargin ({ window } as model) = 77 | if isMobile model then 78 | (window.height - mobileNav.topHeight) // 2 79 | else 80 | window.height // 2 81 | 82 | 83 | renderEvent : Event -> Html Msg 84 | renderEvent event = 85 | div 86 | [ class "ph3 ph4-ns mt3 mb4 mw7 center fade-in flex flex-column items-center" 87 | , id event.url 88 | ] 89 | [ a [ href event.url, class "no-underline dark-green hover-gold tc t-3 all ease", target "_blank" ] [ h3 [ class "mt4 mb3" ] [ text event.title ] ] 90 | , div [ class "flex flex-row items-start w-100" ] 91 | [ whenDetails event 92 | , whereDetails event 93 | , whoDetails event 94 | ] 95 | ] 96 | 97 | 98 | whenDetails : Event -> Html Msg 99 | whenDetails event = 100 | div [ classes [ "gold", iconContainerClasses ] ] 101 | [ h3 [ class "f6" ] [ text "WHEN?" ] 102 | , div [ style [ ( "width", "30px" ) ] ] [ responsiveImg "/images/calendar.svg" ] 103 | , p [ class "f6" ] [ text <| displayDate event.time ] 104 | ] 105 | 106 | 107 | whereDetails : Event -> Html Msg 108 | whereDetails event = 109 | div 110 | [ classes [ "green pointer", iconContainerClasses ] 111 | , onClick <| CenterEvent (makeMarker event) 112 | ] 113 | [ h3 [ class "f6" ] [ text "WHERE?" ] 114 | , div 115 | [ style 116 | [ ( "width", "35px" ) 117 | , ( "height", "33px" ) 118 | ] 119 | , class "spin" 120 | ] 121 | [ responsiveImg "/images/crosshair.svg" ] 122 | , venueAddress event 123 | ] 124 | 125 | 126 | whoDetails : Event -> Html Msg 127 | whoDetails event = 128 | div [ classes [ "light-red", iconContainerClasses ] ] 129 | [ h3 [ class "f6" ] [ text "WHO?" ] 130 | , div [ style [ ( "width", "30px" ) ] ] [ responsiveImg "/images/group.svg" ] 131 | , p [ class "f6" ] [ text event.groupName ] 132 | ] 133 | 134 | 135 | iconContainerClasses : String 136 | iconContainerClasses = 137 | "w-33 flex flex-column justify-center items-center ph2 ph0-ns tc" 138 | 139 | 140 | venueAddress : Event -> Html Msg 141 | venueAddress event = 142 | if isPrivateEvent event then 143 | p [ class "orange f6" ] [ text "join the meetup group to see the location" ] 144 | else 145 | div [ style [ ( "margin-top", "0.65em" ) ] ] 146 | [ span [ class "f6" ] [ text <| event.venueName ++ ", " ] 147 | , br [] [] 148 | , span [ class "f6" ] [ text event.address ] 149 | ] 150 | -------------------------------------------------------------------------------- /assets/elm/Views/Layout.elm: -------------------------------------------------------------------------------- 1 | module Views.Layout exposing (..) 2 | 3 | import Data.Events exposing (stillLoading) 4 | import Helpers.Style exposing (..) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Types exposing (..) 8 | import Views.Navigation exposing (bottomNav, logo, topNav) 9 | 10 | 11 | layout : Model -> Html Msg -> Html Msg 12 | layout model content = 13 | div [ class "flex-ns" ] 14 | [ topNav model 15 | , div 16 | [ class "w-100 ml6-ns pl4-ns flex items-center justify-center" 17 | , style [ handleMobileHeight model ] 18 | ] 19 | [ mobileContainer model content 20 | , div 21 | [ class anchorBottom 22 | , style [ ( "margin-left", "11.5em" ) ] 23 | ] 24 | [ desktopCredit ] 25 | ] 26 | , bottomNav model 27 | ] 28 | 29 | 30 | desktopCredit : Html msg 31 | desktopCredit = 32 | a 33 | [ href "http://www.wearecast.org.uk/" 34 | , target "_blank" 35 | , classes [ "green no-underline f6", desktopOnly ] 36 | ] 37 | [ p [] [ text "made with love at CAST" ] ] 38 | 39 | 40 | handleMobileHeight : Model -> Style 41 | handleMobileHeight model = 42 | mobileFullHeight model |> ifMobile model 43 | 44 | 45 | mobileContainer : Model -> Html Msg -> Html Msg 46 | mobileContainer model content = 47 | if isMobile model then 48 | div [ style [ percentScreenHeight 86 model ] ] [ content ] 49 | else 50 | content 51 | 52 | 53 | loadingScreen : Model -> Html msg 54 | loadingScreen model = 55 | if stillLoading model then 56 | loading "o-100" model 57 | else 58 | loading "o-0 disabled t-delay-500ms" model 59 | 60 | 61 | loading : String -> Model -> Html msg 62 | loading extraClasses model = 63 | div 64 | [ classes 65 | [ "fixed t-500ms bg-green w-100 white z-999 flex items-center justify-center h-100" 66 | , extraClasses 67 | ] 68 | ] 69 | [ div [ class "flex items-center flex-column" ] 70 | [ logo 71 | , p [ class "mt3" ] [ text "Fetching tech for good events" ] 72 | ] 73 | ] 74 | -------------------------------------------------------------------------------- /assets/elm/Views/Location.elm: -------------------------------------------------------------------------------- 1 | module Views.Location exposing (..) 2 | 3 | import Data.Location.Postcode exposing (validPostcode) 4 | import Helpers.Html exposing (emptyProperty, onEnter, responsiveImg) 5 | import Helpers.Style exposing (classes, px) 6 | import Html exposing (..) 7 | import Html.Attributes exposing (..) 8 | import Html.Events exposing (..) 9 | import RemoteData exposing (WebData, RemoteData(..)) 10 | import Types exposing (..) 11 | 12 | 13 | eventsNearPostcode : Model -> Html Msg 14 | eventsNearPostcode model = 15 | div [ class "white t-3 all ease mt4" ] 16 | [ p [] [ text "events near:" ] 17 | , input 18 | [ style [ ( "width", px 100 ) ] 19 | , class "placeholder-white bg-green ba b--white br2 outline-0 f6 fw4 tl pv1 ph2 white" 20 | , onInput UpdatePostcode 21 | , placeholder "postcode" 22 | , fetchOnEnter model.postcode 23 | , extractPostcode model.postcode 24 | ] 25 | [] 26 | , search model 27 | ] 28 | 29 | 30 | search : Model -> Html Msg 31 | search model = 32 | div [ class "mt2 flex justify-between" ] 33 | [ fetchEventsForPostcode model.postcode 34 | , clearUserLocation model.userLocation 35 | ] 36 | 37 | 38 | fetchEventsForPostcode : Postcode -> Html Msg 39 | fetchEventsForPostcode postcode = 40 | if validPostcode postcode then 41 | div 42 | [ onClick FetchEventsForPostcode 43 | , class "pointer fade-in o-0" 44 | ] 45 | [ text "search" ] 46 | else 47 | span [ class "w1" ] [] 48 | 49 | 50 | clearUserLocation : WebData Coords -> Html Msg 51 | clearUserLocation userLocation = 52 | case userLocation of 53 | NotAsked -> 54 | span [ class "w1" ] [] 55 | 56 | _ -> 57 | div 58 | [ onClick ClearUserLocation 59 | , class "pointer mr1 fade-in o-0" 60 | ] 61 | [ text "clear" ] 62 | 63 | 64 | fetchOnEnter : Postcode -> Attribute Msg 65 | fetchOnEnter postcode = 66 | if validPostcode postcode then 67 | onEnter FetchEventsForPostcode 68 | else 69 | emptyProperty 70 | 71 | 72 | extractPostcode : Postcode -> Attribute Msg 73 | extractPostcode x = 74 | case x of 75 | NotEntered -> 76 | value "" 77 | 78 | Valid postcode -> 79 | value <| String.toUpper postcode 80 | 81 | Invalid postcode -> 82 | value <| String.toUpper postcode 83 | -------------------------------------------------------------------------------- /assets/elm/Views/Map.elm: -------------------------------------------------------------------------------- 1 | module Views.Map exposing (..) 2 | 3 | import Config 4 | import Data.Events exposing (filterEvents, numberVisibleEvents) 5 | import Helpers.Html exposing (emptyProperty) 6 | import Helpers.Style exposing (classes, isMobile, px) 7 | import Html exposing (..) 8 | import Html.Attributes exposing (..) 9 | import Html.Events exposing (onClick) 10 | import Types exposing (..) 11 | 12 | 13 | renderMap : Model -> Html Msg 14 | renderMap model = 15 | div 16 | [ classes [ "flex w-100 z-5", mapPositioning model ] 17 | , style [ ( "height", px <| mapHeight model ) ] 18 | , handleHideMobileDateOptions model 19 | ] 20 | [ div [ class "ml6-ns pl4-ns" ] [] 21 | , div [ id Config.mapId, class mapBaseClasses ] [] 22 | ] 23 | 24 | 25 | handleHideMobileDateOptions : Model -> Attribute Msg 26 | handleHideMobileDateOptions model = 27 | if isMobile model then 28 | onClick ResetMobileNav 29 | else 30 | emptyProperty 31 | 32 | 33 | mapHeight : Model -> Int 34 | mapHeight ({ window } as model) = 35 | let 36 | { topHeight, bottomHeight } = 37 | Config.mobileNav 38 | in 39 | if isMobile model then 40 | if model.bottomNavOpen then 41 | ((window.height - topHeight) // 2) - bottomHeight 42 | else 43 | window.height - topHeight - bottomHeight 44 | else 45 | window.height // 2 46 | 47 | 48 | mapPositioning : Model -> String 49 | mapPositioning model = 50 | if numberVisibleEvents model > 0 then 51 | "fixed" 52 | else 53 | "absolute" 54 | 55 | 56 | mapBaseClasses : String 57 | mapBaseClasses = 58 | "fade-in bg-light-gray w-100" 59 | -------------------------------------------------------------------------------- /assets/elm/Views/Navigation.elm: -------------------------------------------------------------------------------- 1 | module Views.Navigation exposing (..) 2 | 3 | import Config exposing (mobileNav) 4 | import Helpers.Html exposing (responsiveImg) 5 | import Helpers.Style exposing (..) 6 | import Html exposing (..) 7 | import Html.Attributes exposing (..) 8 | import Html.Events exposing (..) 9 | import Types exposing (..) 10 | import Views.Dates exposing (..) 11 | import Views.Location exposing (eventsNearPostcode) 12 | 13 | 14 | topNav : Model -> Html Msg 15 | topNav model = 16 | nav [] 17 | [ desktopNavbar model 18 | , mobileTopBar model 19 | ] 20 | 21 | 22 | bottomNav : Model -> Html Msg 23 | bottomNav model = 24 | nav [ class mobileOnly ] 25 | [ mobileBottomNav model 26 | ] 27 | 28 | 29 | desktopNavbar : Model -> Html Msg 30 | desktopNavbar model = 31 | div [ class desktopOnly ] 32 | [ div [ class "flex justify-between white pa3 pb3 bg-green fixed h-100 w5 dib left-0 top-0 z-5" ] 33 | [ div [] 34 | [ div [ class "pointer" ] 35 | [ logo 36 | , p [ class "mt0 ml1" ] [ text "near you" ] 37 | ] 38 | , navbarOptions model 39 | , div 40 | [ class "absolute left-0 right-0 ph3" 41 | , style [ ( "bottom", "-0.3em" ) ] 42 | ] 43 | [ techForGoodSummerDesktop model ] 44 | ] 45 | ] 46 | ] 47 | 48 | 49 | techForGoodSummerDesktop : Model -> Html Msg 50 | techForGoodSummerDesktop model = 51 | googleSheetLink 52 | (div 53 | [ classes [ "w-100 ph2", hideWhenShortScreen model ] ] 54 | [ responsiveImg "/images/tech-for-good-summer.png" ] 55 | ) 56 | 57 | 58 | techForGoodSummerMobile : Html Msg 59 | techForGoodSummerMobile = 60 | googleSheetLink 61 | (div [ class "w-100 ph2" ] [ responsiveImg "/images/tech-for-good-summer.png" ]) 62 | 63 | 64 | googleSheetLink : Html Msg -> Html Msg 65 | googleSheetLink image = 66 | a 67 | [ class "no-underline white tc db" 68 | , href "/user-event/new" 69 | ] 70 | [ p [ class "f5 f6-ns" ] [ text "Add your own Tech for Good event!" ] 71 | , image 72 | ] 73 | 74 | 75 | mobileTopBar : Model -> Html Msg 76 | mobileTopBar model = 77 | div [ class mobileOnly, style [ ( "margin-bottom", px mobileNav.topHeight ) ] ] 78 | [ div 79 | [ class "fixed z-5 bg-green white top-0 left-0 w-100 flex justify-between items-center" 80 | , style [ ( "height", px mobileNav.topHeight ) ] 81 | ] 82 | [ div [ class "ml2 mt2 pointer flex" ] [ logo, p [ class "ml2" ] [ text "near you" ] ] 83 | , div 84 | [ style [ ( "width", "20px" ), plusIconRotation model ] 85 | , class "mr3 pointer" 86 | , onClick ToggleTopNavbar 87 | ] 88 | [ responsiveImg "/images/plus.png" ] 89 | ] 90 | , mobileTopBarContent model 91 | ] 92 | 93 | 94 | plusIconRotation : Model -> Style 95 | plusIconRotation model = 96 | if model.topNavOpen then 97 | transform <| rotateZ 45 98 | else 99 | transform <| rotateZ 0 100 | 101 | 102 | mobileTopBarContent : Model -> Html Msg 103 | mobileTopBarContent model = 104 | if model.topNavOpen then 105 | div 106 | [ class "w-100 bg-green flex items-center justify-center flex-column white fixed z-999 ph3 fade-in a-3" 107 | , style [ mobileMaxHeight model ] 108 | ] 109 | [ div [ class "ph4 mb5" ] [ techForGoodSummerMobile ] 110 | , a [ href "http://www.wearecast.org.uk/", target "_blank", class "no-underline white db" ] [ p [] [ text "made with love at CAST" ] ] 111 | ] 112 | else 113 | span [] [] 114 | 115 | 116 | mobileBottomNav : Model -> Html Msg 117 | mobileBottomNav model = 118 | div 119 | [ classes [ "bg-green w-100 fixed left-0 bottom-0 z-5 flex items-center justify-center" ] 120 | , style 121 | [ ( "height", px mobileNav.bottomHeight ) 122 | , bottomMobileNavPosition model 123 | ] 124 | ] 125 | [ mobileBottomNavOptions model ] 126 | 127 | 128 | bottomMobileNavPosition : Model -> Style 129 | bottomMobileNavPosition model = 130 | if model.bottomNavOpen then 131 | transform <| translateY <| (model.window.height - mobileNav.topHeight) // -2 132 | else 133 | transform <| translateY 0 134 | 135 | 136 | mobileBottomNavOptions : Model -> Html Msg 137 | mobileBottomNavOptions model = 138 | if model.mobileDateOptionsVisible then 139 | mobileDateOptions model 140 | else 141 | mobileMainOptions model 142 | 143 | 144 | mobileMainOptions : Model -> Html Msg 145 | mobileMainOptions model = 146 | div [ class "flex items-center justify-between w-100 ph5" ] 147 | [ div 148 | [ style [ ( "width", "25px" ) ] 149 | , class "pointer" 150 | , onClick <| MobileDateVisible True 151 | ] 152 | [ responsiveImg "/images/clock.svg" ] 153 | , div 154 | [ style [ ( "width", "25px" ) ] 155 | , class "pointer" 156 | , handleBottomNavToggle model 157 | ] 158 | [ responsiveImg "/images/calendar-white.svg" ] 159 | ] 160 | 161 | 162 | mobileDateOptions : Model -> Html Msg 163 | mobileDateOptions model = 164 | div [ class "flex items-center justify-between w-100 ph3" ] 165 | [ div 166 | [ class "absolute left-0 top-0" 167 | , style [ ( "margin-left", "0.5rem" ), ( "margin-top", "0.5em" ) ] 168 | ] 169 | [ dateBottomBarOptions model ] 170 | , div 171 | [ onClick <| MobileDateVisible False 172 | , style 173 | [ transform <| rotateZ 45 174 | , ( "width", "20px" ) 175 | , ( "margin-right", "0.7rem" ) 176 | , ( "margin-top", "1em" ) 177 | ] 178 | , class "absolute right-0 top-0" 179 | ] 180 | [ responsiveImg "/images/plus.png" ] 181 | ] 182 | 183 | 184 | handleBottomNavToggle : Model -> Attribute Msg 185 | handleBottomNavToggle model = 186 | if model.bottomNavOpen then 187 | onClick <| BottomNavOpen False 188 | else 189 | onClick <| BottomNavOpen True 190 | 191 | 192 | logo : Html msg 193 | logo = 194 | div [ class "w3 w4-ns" ] [ responsiveImg "/images/tech-for-good.png" ] 195 | 196 | 197 | navbarOptions : Model -> Html Msg 198 | navbarOptions model = 199 | div [ class "pt1 pb3-ns t-500ms all ease bg-green" ] 200 | [ dateSideOptions model 201 | , eventsNearPostcode model 202 | ] 203 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import 'phoenix_html' 2 | 3 | import ElmApp from './elm.js' 4 | import elmEmbed from './elm-embed.js' 5 | 6 | if (window.ElmApp.boot) { 7 | elmEmbed.init(ElmApp) 8 | } 9 | -------------------------------------------------------------------------------- /assets/js/elm-embed.js: -------------------------------------------------------------------------------- 1 | import { 2 | initMap, 3 | updateMarkers, 4 | updateUserLocation, 5 | clearUserLocation, 6 | centerMapOnUser, 7 | centerEvent, 8 | fitBounds, 9 | resizeMap 10 | } from './gmap' 11 | 12 | function init (Elm) { 13 | var node = document.getElementById('elm-app') 14 | var app = Elm.App.embed(node) 15 | 16 | app.ports.initMap.subscribe(initMap) 17 | app.ports.updateMarkers.subscribe(updateMarkers(app)) 18 | app.ports.updateUserLocation.subscribe(updateUserLocation) 19 | app.ports.clearUserLocation_.subscribe(clearUserLocation) 20 | app.ports.centerMapOnUser_.subscribe(centerMapOnUser) 21 | app.ports.centerEvent.subscribe(centerEvent) 22 | app.ports.fitBounds_.subscribe(fitBounds) 23 | app.ports.resizeMap_.subscribe(resizeMap) 24 | } 25 | 26 | module.exports = { init } 27 | -------------------------------------------------------------------------------- /assets/js/gmap.js: -------------------------------------------------------------------------------- 1 | import { sendScrollDistanceToElm } from './interop.js' 2 | 3 | const google = window.google 4 | let _map 5 | let infoWindow 6 | let mapDiv 7 | let userLocation 8 | let visibleMarkers = [] 9 | 10 | function initMap ({ marker, mapId }) { 11 | var mapOptions = { 12 | zoom: 10, 13 | center: { 14 | lat: marker.lat, 15 | lng: marker.lng 16 | }, 17 | gestureHandling: 'greedy', 18 | mapTypeControl: false, 19 | streetViewControl: false 20 | } 21 | 22 | mapDiv = document.getElementById(mapId) 23 | _map = new google.maps.Map(mapDiv, mapOptions) 24 | infoWindow = new google.maps.InfoWindow() 25 | } 26 | 27 | 28 | function makeMarker (options) { 29 | var _options = { 30 | map: _map, 31 | animation: google.maps.Animation.DROP, 32 | position: { 33 | lat: options.lat, 34 | lng: options.lng 35 | } 36 | } 37 | 38 | return { 39 | url: options.url, 40 | title: options.title, 41 | instance: new google.maps.Marker(_options) 42 | } 43 | } 44 | 45 | function clearVisibleMarkers () { 46 | visibleMarkers.map(m => m.instance.setMap(null)) 47 | visibleMarkers = [] 48 | } 49 | 50 | function makeDescription (_marker) { 51 | return ` 52 |
53 | 54 |

${_marker.title}

55 |
56 |
57 | ` 58 | } 59 | 60 | function fitBounds () { 61 | if (visibleMarkers.length) { 62 | resizeMap() 63 | _fitBounds(visibleMarkers) 64 | } 65 | } 66 | 67 | function _fitBounds (_markers) { 68 | var bounds = new google.maps.LatLngBounds() 69 | _markers.forEach(m => bounds.extend(m.instance.getPosition())) 70 | _userLocationBounds(bounds) 71 | _map.fitBounds(bounds) 72 | } 73 | 74 | function _userLocationBounds (bounds) { 75 | if (userLocation) { 76 | bounds.extend(userLocation.getPosition()) 77 | } 78 | } 79 | 80 | function addMarkerListener (elmApp, _marker) { 81 | google.maps.event.addListener(_marker.instance, 'click', function () { 82 | sendScrollDistanceToElm(elmApp, _marker) 83 | infoWindow.setContent(makeDescription(_marker)) 84 | infoWindow.open(_map, this) 85 | }) 86 | } 87 | 88 | function normalizeZoom (n) { 89 | if (_map.getZoom() > n) { 90 | _map.setZoom(n) 91 | } 92 | } 93 | 94 | function updateMarkers (elmApp) { 95 | return function (newMarkers) { 96 | clearVisibleMarkers() 97 | newMarkers.forEach(m => visibleMarkers.push(makeMarker(m))) 98 | visibleMarkers.forEach(m => addMarkerListener(elmApp, m)) 99 | 100 | if (visibleMarkers.length > 0) { 101 | _fitBounds(visibleMarkers) 102 | resizeMap() 103 | normalizeZoom(13) 104 | } 105 | } 106 | } 107 | 108 | function updateUserLocation (coords) { 109 | var _options = { 110 | map: _map, 111 | icon: 'https://cloud.githubusercontent.com/assets/14013616/23849995/8989fe0a-07d5-11e7-9e81-c3786679d312.png', 112 | position: { 113 | lat: coords.lat, 114 | lng: coords.lng 115 | } 116 | } 117 | clearUserLocation() 118 | userLocation = new google.maps.Marker(_options) 119 | } 120 | 121 | function clearUserLocation () { 122 | if (userLocation) { 123 | userLocation.setMap(null) 124 | } 125 | } 126 | 127 | function centerMapOnUser () { 128 | if (userLocation) { 129 | _map.setCenter(userLocation.getPosition()) 130 | _map.setZoom(13) 131 | } 132 | } 133 | 134 | function centerEvent (event) { 135 | var selectedMarkerArr = visibleMarkers.filter(function (marker) { 136 | return marker.title === event.title 137 | }) 138 | 139 | var selectedMarker = selectedMarkerArr.length 140 | ? selectedMarkerArr[0].instance 141 | : {} 142 | 143 | _map.setCenter(event) 144 | _map.setZoom(16) 145 | infoWindow.setContent(makeDescription(event)) 146 | infoWindow.open(_map, selectedMarker) 147 | } 148 | 149 | function resizeMap () { 150 | google.maps.event.trigger(_map, 'resize') 151 | } 152 | 153 | module.exports = { 154 | initMap, 155 | updateMarkers, 156 | updateUserLocation, 157 | clearUserLocation, 158 | centerMapOnUser, 159 | centerEvent, 160 | resizeMap, 161 | fitBounds 162 | } 163 | -------------------------------------------------------------------------------- /assets/js/interop.js: -------------------------------------------------------------------------------- 1 | function sendScrollDistanceToElm (app, _marker) { 2 | const el = document.getElementById(_marker.url) 3 | app.ports.scrollToEvent.send(el.offsetTop) 4 | } 5 | 6 | module.exports = { sendScrollDistanceToElm } 7 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "build-elm": "elm-package install -y && elm-make elm/App.elm --output=js/elm.js", 7 | "watch": "brunch watch --stdin" 8 | }, 9 | "dependencies": { 10 | "elm": "^0.18.0", 11 | "phoenix": "file:../deps/phoenix", 12 | "phoenix_html": "file:../deps/phoenix_html", 13 | "tachyons-custom": "^4.6.0" 14 | }, 15 | "devDependencies": { 16 | "autoprefixer": "^7.1.1", 17 | "babel-brunch": "6.0.6", 18 | "brunch": "2.10.7", 19 | "clean-css-brunch": "2.10.0", 20 | "elm-brunch": "^0.9.0", 21 | "postcss-brunch": "^2.0.5", 22 | "postcss-clean": "^1.0.2", 23 | "postcss-custom-media": "^6.0.0", 24 | "postcss-custom-properties": "^6.0.1", 25 | "postcss-import": "^10.0.0", 26 | "uglify-js-brunch": "2.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechforgoodCAST/tech-for-good-near-you/80e4d036cd866ba0da92969e2838dc8e8dfe5fe9/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/calendar-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/crosshair-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/crosshair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechforgoodCAST/tech-for-good-near-you/80e4d036cd866ba0da92969e2838dc8e8dfe5fe9/assets/static/images/plus.png -------------------------------------------------------------------------------- /assets/static/images/tech-for-good-summer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechforgoodCAST/tech-for-good-near-you/80e4d036cd866ba0da92969e2838dc8e8dfe5fe9/assets/static/images/tech-for-good-summer.png -------------------------------------------------------------------------------- /assets/static/images/tech-for-good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechforgoodCAST/tech-for-good-near-you/80e4d036cd866ba0da92969e2838dc8e8dfe5fe9/assets/static/images/tech-for-good.png -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /compile_static.sh: -------------------------------------------------------------------------------- 1 | cd $phoenix_dir 2 | cd assets 3 | npm run build-elm && npm run deploy 4 | cd .. 5 | mix "${phoenix_ex}.digest" 6 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | config :tech_for_good_near_you, 9 | meetup_group_ids: [ 10 | "1277423", 11 | "19866282", 12 | "20232146", 13 | "18854399", 14 | "1302479", 15 | "16347132", 16 | "22407110", 17 | "12205442", 18 | "22082866", 19 | "2503312", 20 | "7975692", 21 | "19414181", 22 | "18542782", 23 | "1635343", 24 | "18436868", 25 | "19911171", 26 | "22216274", 27 | "18976100", 28 | "17833522", 29 | "18037392", 30 | "19201419", 31 | "22434994", 32 | "20399973", 33 | "22283959", 34 | "14592582", 35 | "11072312", 36 | "466780", 37 | "11972762", 38 | "20791546", 39 | "18509845", 40 | "22894458", 41 | "26967906" 42 | ] 43 | 44 | # General application configuration 45 | config :tech_for_good_near_you, 46 | ecto_repos: [TechForGoodNearYou.Repo] 47 | 48 | # Configures the endpoint 49 | config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 50 | url: [host: "localhost"], 51 | secret_key_base: "/j2KYOATdpxnRYMNtwiMoQwycA577AbuY05ToTl4unVEX1Sg/TWra1DnBZ8HtrCn", 52 | render_errors: [view: TechForGoodNearYou.Web.ErrorView, accepts: ~w(html json)], 53 | pubsub: [name: TechForGoodNearYou.PubSub, 54 | adapter: Phoenix.PubSub.PG2] 55 | 56 | # Configures Elixir's Logger 57 | config :logger, :console, 58 | format: "$time $metadata[$level] $message\n", 59 | metadata: [:request_id] 60 | 61 | # Import environment specific config. This must remain at the bottom 62 | # of this file so it overrides the configuration defined above. 63 | import_config "#{Mix.env}.exs" 64 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../assets", __DIR__)]] 16 | 17 | # ## SSL Support 18 | # 19 | # In order to use HTTPS in development, a self-signed 20 | # certificate can be generated by running the following 21 | # command from your terminal: 22 | # 23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 24 | # 25 | # The `http:` config above can be replaced with: 26 | # 27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 28 | # 29 | # If desired, both `http:` and `https:` keys can be 30 | # configured to run both http and https servers on 31 | # different ports. 32 | 33 | # Watch static and templates for browser reloading. 34 | config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 35 | live_reload: [ 36 | patterns: [ 37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 38 | ~r{priv/gettext/.*(po)$}, 39 | ~r{lib/tech_for_good_near_you/web/views/.*(ex)$}, 40 | ~r{lib/tech_for_good_near_you/web/templates/.*(eex)$} 41 | ] 42 | ] 43 | 44 | # Do not include metadata nor timestamps in development logs 45 | config :logger, :console, format: "[$level] $message\n" 46 | 47 | # Set a higher stacktrace during development. Avoid configuring such 48 | # in production as building large stacktraces may be expensive. 49 | config :phoenix, :stacktrace_depth, 20 50 | 51 | # Configure your database 52 | config :tech_for_good_near_you, TechForGoodNearYou.Repo, 53 | adapter: Ecto.Adapters.Postgres, 54 | username: "postgres", 55 | password: "postgres", 56 | database: "tech_for_good_near_you_dev", 57 | hostname: "localhost", 58 | pool_size: 10 59 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # TechForGoodNearYou.Web.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 17 | http: [port: {:system, "PORT"}], 18 | load_from_system_env: true, 19 | url: [scheme: "https", host: "tech-for-good-near-you.herokuapp.com", port: 443], 20 | force_ssl: [rewrite_on: [:x_forwarded_proto]], 21 | cache_static_manifest: "priv/static/cache_manifest.json", 22 | secret_key_base: System.get_env("SECRET_KEY_BASE") 23 | # Do not print debug messages in production 24 | config :logger, level: :info 25 | 26 | # Configure your database 27 | config :tech_for_good_near_you, TechForGoodNearYou.Repo, 28 | adapter: Ecto.Adapters.Postgres, 29 | url: System.get_env("DATABASE_URL"), 30 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 31 | ssl: true 32 | 33 | # ## SSL Support 34 | # 35 | # To get SSL working, you will need to add the `https` key 36 | # to the previous section and set your `:url` port to 443: 37 | # 38 | # config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 39 | # ... 40 | # url: [host: "example.com", port: 443], 41 | # https: [:inet6, 42 | # port: 443, 43 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 44 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 45 | # 46 | # Where those two env variables return an absolute path to 47 | # the key and cert in disk or a relative path inside priv, 48 | # for example "priv/ssl/server.key". 49 | # 50 | # We also recommend setting `force_ssl`, ensuring no data is 51 | # ever sent via http, always redirecting to https: 52 | # 53 | # config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 54 | # force_ssl: [hsts: true] 55 | # 56 | # Check `Plug.SSL` for all available options in `force_ssl`. 57 | 58 | # ## Using releases 59 | # 60 | # If you are doing OTP releases, you need to instruct Phoenix 61 | # to start the server for all endpoints: 62 | # 63 | # config :phoenix, :serve_endpoints, true 64 | # 65 | # Alternatively, you can configure exactly which server to 66 | # start per endpoint: 67 | # 68 | # config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, server: true 69 | # 70 | 71 | # Finally import the config/prod.secret.exs 72 | # which should be versioned separately. 73 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :tech_for_good_near_you, TechForGoodNearYou.Web.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :tech_for_good_near_you, TechForGoodNearYou.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "tech_for_good_near_you_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/accounts/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Accounts do 2 | @moduledoc """ 3 | The boundary for the Accounts system. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias TechForGoodNearYou.Repo 8 | 9 | alias TechForGoodNearYou.Accounts.Admin 10 | 11 | def get_admin(id), do: Repo.get(Admin, id) 12 | def get_admin_by(opts), do: Repo.get_by(Admin, opts) 13 | end 14 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/accounts/admin.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Accounts.Admin do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias TechForGoodNearYou.Accounts.Admin 5 | 6 | 7 | schema "accounts_admins" do 8 | field :password, :string 9 | field :username, :string 10 | end 11 | 12 | @doc false 13 | def changeset(%Admin{} = admin, attrs) do 14 | admin 15 | |> cast(attrs, [:username, :password]) 16 | |> validate_required([:username, :password]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/application.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(TechForGoodNearYou.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(TechForGoodNearYou.Web.Endpoint, []), 15 | # Start your own worker by calling: TechForGoodNearYou.Worker.start_link(arg1, arg2, arg3) 16 | # worker(TechForGoodNearYou.Worker, [arg1, arg2, arg3]), 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: TechForGoodNearYou.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/meet_ups/event.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.MeetUps.Event do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias TechForGoodNearYou.MeetUps.Event 5 | 6 | 7 | schema "meet_ups_events" do 8 | field :name, :string 9 | field :url, :string 10 | field :time, :naive_datetime 11 | field :address, :string 12 | field :postcode, :string 13 | field :venue_name, :string 14 | field :group_name, :string 15 | field :latitude, :float 16 | field :longitude, :float 17 | field :approved, :boolean 18 | 19 | timestamps() 20 | end 21 | 22 | @valid_fields [:name, 23 | :url, 24 | :time, 25 | :address, 26 | :postcode, 27 | :venue_name, 28 | :group_name, 29 | :latitude, 30 | :longitude, 31 | :approved] 32 | 33 | @required_fields [:name, 34 | :time, 35 | :address, 36 | :postcode, 37 | :url, 38 | :approved] 39 | 40 | @valid_postcode_regex ~r/(GIR 0AA)|([A-PR-UWYZ](([0-9]([0-9A-HJKPSTUW])?)|([A-HK-Y][0-9]([0-9ABEHMNPRVWXY])?))\s?[0-9][ABD-HJLNP-UW-Z]{2})/i 41 | 42 | def changeset(%Event{} = event, attrs) do 43 | event 44 | |> cast(attrs, @valid_fields) 45 | |> validate_required(@required_fields) 46 | |> validate_format(:postcode, @valid_postcode_regex) 47 | end 48 | 49 | def validate_postcode_changeset(%Event{} = event, attrs) do 50 | event 51 | |> cast(attrs, [:postcode]) 52 | |> validate_format(:postcode, @valid_postcode_regex) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/meet_ups/meet_ups.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.MeetUps do 2 | @moduledoc """ 3 | The boundary for the MeetUps system. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias TechForGoodNearYou.Repo 8 | 9 | alias TechForGoodNearYou.MeetUps.Event 10 | 11 | @doc """ 12 | Returns the list of events. 13 | 14 | ## Examples 15 | 16 | iex> list_events() 17 | [%Event{}, ...] 18 | 19 | """ 20 | def list_events do 21 | Repo.all(Event) 22 | end 23 | 24 | @doc """ 25 | Returns the list of future events. 26 | 27 | ## Examples 28 | 29 | iex> list_future_events() 30 | [%Event{}, ...] 31 | 32 | """ 33 | 34 | def list_approved_events do 35 | query = from e in Event, 36 | where: e.time > from_now(0, "second"), 37 | where: e.approved == true 38 | 39 | Repo.all(query) 40 | end 41 | 42 | def list_events_waiting_approval do 43 | query = from e in Event, 44 | where: e.time > from_now(0, "second"), 45 | where: e.approved == false 46 | 47 | Repo.all(query) 48 | end 49 | 50 | 51 | @doc """ 52 | Gets a single event. 53 | 54 | Raises `Ecto.NoResultsError` if the Event does not exist. 55 | 56 | ## Examples 57 | 58 | iex> get_event!(123) 59 | %Event{} 60 | 61 | iex> get_event!(456) 62 | ** (Ecto.NoResultsError) 63 | 64 | """ 65 | def get_event!(id), do: Repo.get!(Event, id) 66 | 67 | @doc """ 68 | Creates a event. 69 | 70 | ## Examples 71 | 72 | iex> create_event(%{field: value}) 73 | {:ok, %Event{}} 74 | 75 | iex> create_event(%{field: bad_value}) 76 | {:error, %Ecto.Changeset{}} 77 | 78 | """ 79 | def create_event(attrs \\ %{}) do 80 | %Event{} 81 | |> Event.changeset(attrs) 82 | |> Repo.insert() 83 | end 84 | 85 | @doc """ 86 | Updates a event. 87 | 88 | ## Examples 89 | 90 | iex> update_event(event, %{field: new_value}) 91 | {:ok, %Event{}} 92 | 93 | iex> update_event(event, %{field: bad_value}) 94 | {:error, %Ecto.Changeset{}} 95 | 96 | """ 97 | def update_event(%Event{} = event, attrs) do 98 | event 99 | |> Event.changeset(attrs) 100 | |> Repo.update() 101 | end 102 | 103 | @doc """ 104 | Deletes a Event. 105 | 106 | ## Examples 107 | 108 | iex> delete_event(event) 109 | {:ok, %Event{}} 110 | 111 | iex> delete_event(event) 112 | {:error, %Ecto.Changeset{}} 113 | 114 | """ 115 | def delete_event(%Event{} = event) do 116 | Repo.delete(event) 117 | end 118 | 119 | @doc """ 120 | Returns an `%Ecto.Changeset{}` for tracking event changes. 121 | 122 | ## Examples 123 | 124 | iex> change_event(event) 125 | %Ecto.Changeset{source: %Event{}} 126 | 127 | """ 128 | def change_event(%Event{} = event) do 129 | Event.changeset(event, %{}) 130 | end 131 | 132 | def validate_postcode(params) do 133 | change = 134 | %Event{} 135 | |> Event.validate_postcode_changeset(params) 136 | |> update_changeset_action(:validate_postcode) 137 | 138 | case change do 139 | %Ecto.Changeset{valid?: true} = changeset -> 140 | {:ok, changeset} 141 | changeset -> 142 | {:error, changeset} 143 | end 144 | end 145 | 146 | defp update_changeset_action(changeset, action) do 147 | %{changeset | action: action} 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo do 2 | use Ecto.Repo, otp_app: :tech_for_good_near_you 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/controllers/elm_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ElmController do 2 | use TechForGoodNearYou.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/controllers/event_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.EventController do 2 | use TechForGoodNearYou.Web, :controller 3 | 4 | alias TechForGoodNearYou.{MeetUps, Web.LatLon} 5 | 6 | def index(conn, _params) do 7 | approved_events = MeetUps.list_approved_events() 8 | events_waiting_approval = MeetUps.list_events_waiting_approval() 9 | render( 10 | conn, 11 | "index.html", 12 | approved_events: approved_events, 13 | events_waiting_approval: events_waiting_approval 14 | ) 15 | end 16 | 17 | def new(conn, _params) do 18 | changeset = MeetUps.change_event(%TechForGoodNearYou.MeetUps.Event{}) 19 | render(conn, "new.html", changeset: changeset) 20 | end 21 | 22 | def create(conn, %{"event" => event_params}) do 23 | with {:ok, _changeset} <- MeetUps.validate_postcode(event_params), 24 | {:ok, lat_lon} <- LatLon.get_lat_lon(event_params["postcode"]), 25 | event_params = Map.merge(event_params, lat_lon), 26 | event_params = Map.put_new(event_params, "approved", true), 27 | {:ok, event} <- MeetUps.create_event(event_params) 28 | do 29 | conn 30 | |> put_flash(:info, "Event created successfully.") 31 | |> redirect(to: event_path(conn, :show, event)) 32 | else 33 | {:error, %Ecto.Changeset{} = changeset} -> 34 | render(conn, "new.html", changeset: changeset) 35 | {:error, _reason} -> 36 | conn 37 | |> put_flash(:error, "There was a problem adding the event, please try again") 38 | |> redirect(to: event_path(conn, :new)) 39 | end 40 | end 41 | 42 | def show(conn, %{"id" => id}) do 43 | event = MeetUps.get_event!(id) 44 | render(conn, "show.html", event: event) 45 | end 46 | 47 | def edit(conn, %{"id" => id}) do 48 | event = MeetUps.get_event!(id) 49 | changeset = MeetUps.change_event(event) 50 | render(conn, "edit.html", event: event, changeset: changeset) 51 | end 52 | 53 | def update(conn, %{"id" => id, "event" => %{"postcode" => postcode} = event_params}) do 54 | event = MeetUps.get_event!(id) 55 | 56 | with {:ok, _changeset} <- MeetUps.validate_postcode(event_params), 57 | {:ok, lat_lon} <- LatLon.get_lat_lon(postcode), 58 | event_params = Map.merge(event_params, lat_lon), 59 | {:ok, event} <- MeetUps.update_event(event, event_params) 60 | do 61 | conn 62 | |> put_flash(:info, "Event updated successfully.") 63 | |> redirect(to: event_path(conn, :show, event)) 64 | else 65 | {:error, %Ecto.Changeset{} = changeset} -> 66 | render(conn, "edit.html", event: event, changeset: changeset) 67 | {:error, _reason} -> 68 | conn 69 | |> put_flash(:error, "There was a problem updating the event, please try again") 70 | |> redirect(to: event_path(conn, :edit, event)) 71 | end 72 | end 73 | 74 | def update(conn, %{"id" => id, "event" => event_params}) do 75 | event = MeetUps.get_event!(id) 76 | 77 | case MeetUps.update_event(event, event_params) do 78 | {:ok, event} -> 79 | conn 80 | |> put_flash(:info, "Event updated successfully.") 81 | |> redirect(to: event_path(conn, :show, event)) 82 | {:error, %Ecto.Changeset{} = changeset} -> 83 | render(conn, "edit.html", event: event, changeset: changeset) 84 | end 85 | end 86 | 87 | def delete(conn, %{"id" => id}) do 88 | event = MeetUps.get_event!(id) 89 | {:ok, _event} = MeetUps.delete_event(event) 90 | 91 | conn 92 | |> put_flash(:info, "Event deleted successfully.") 93 | |> redirect(to: event_path(conn, :index)) 94 | end 95 | 96 | def custom_events(conn, _params) do 97 | events = MeetUps.list_approved_events() 98 | render conn, "events.json", %{events: events} 99 | end 100 | 101 | def user_event_new(conn, _params) do 102 | changeset = MeetUps.change_event(%TechForGoodNearYou.MeetUps.Event{}) 103 | render(conn, "user_event.html", changeset: changeset, action: event_path(conn, :user_event_create)) 104 | end 105 | 106 | def user_event_create(conn, %{"event" => event_params}) do 107 | with {:ok, _changeset} <- MeetUps.validate_postcode(event_params), 108 | {:ok, lat_lon} <- LatLon.get_lat_lon(event_params["postcode"]), 109 | event_params = Map.merge(event_params, lat_lon), 110 | event_params = Map.put_new(event_params, "approved", false), 111 | {:ok, _event} <- MeetUps.create_event(event_params) 112 | do 113 | conn 114 | |> redirect(to: event_path(conn, :user_event_confirmation)) 115 | else 116 | {:error, %Ecto.Changeset{} = changeset} -> 117 | render(conn, "user_event.html", changeset: changeset) 118 | {:error, _reason} -> 119 | conn 120 | |> put_flash(:error, "There was a problem adding the event, please try again") 121 | |> redirect(to: event_path(conn, :user_event_new)) 122 | end 123 | end 124 | 125 | def user_event_confirmation(conn, _params) do 126 | render(conn, "confirmation.html") 127 | end 128 | 129 | def approve_event(conn, %{"id" => id}) do 130 | event = MeetUps.get_event!(id) 131 | 132 | case MeetUps.update_event(event, %{approved: true}) do 133 | {:ok, _event} -> 134 | conn 135 | |> put_flash(:info, "The event has been approved") 136 | |> redirect(to: event_path(conn, :index)) 137 | {:error, _changeset} -> 138 | conn 139 | |> put_flash(:error, "There was a problem updating the event") 140 | |> redirect(to: event_path(conn, :index)) 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/controllers/meetup_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.MeetupController do 2 | use TechForGoodNearYou.Web, :controller 3 | 4 | def index(conn, _params) do 5 | group_ids = Application.get_env( 6 | :tech_for_good_near_you, 7 | :meetup_group_ids 8 | ) 9 | 10 | response = 11 | HTTPoison.get!( 12 | "https://api.meetup.com/2/events", [], 13 | params: [{"group_id", Enum.join(group_ids, ",")}] 14 | ) 15 | |> Map.get(:body) 16 | |> Poison.decode! 17 | 18 | json(conn, response["results"]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.SessionController do 2 | use TechForGoodNearYou.Web, :controller 3 | alias TechForGoodNearYou.Web.Auth 4 | 5 | def new(conn, _params) do 6 | render(conn, "new.html") 7 | end 8 | 9 | def create(conn, %{"session" => %{"username" => user, "password" => pass}}) do 10 | case Auth.login_by_username_and_pass(conn, user, pass) do 11 | {:ok, conn} -> 12 | conn 13 | |> put_flash(:info, "Welcome back!") 14 | |> redirect(to: event_path(conn, :index)) 15 | {:error, _reason, conn} -> 16 | conn 17 | |> put_flash(:error, "Invalid username/password combination") 18 | |> render("new.html") 19 | end 20 | end 21 | 22 | def delete(conn, _) do 23 | conn 24 | |> Auth.logout() 25 | |> redirect(to: session_path(conn, :new)) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :tech_for_good_near_you 3 | 4 | # Serve at "/" the static files from "priv/static" directory. 5 | # 6 | # You should set gzip to true if you are running phoenix.digest 7 | # when deploying your static files in production. 8 | plug Plug.Static, 9 | at: "/", from: :tech_for_good_near_you, gzip: false, 10 | only: ~w(css fonts images js favicon.ico robots.txt) 11 | 12 | # Code reloading can be explicitly enabled under the 13 | # :code_reloader configuration of your endpoint. 14 | if code_reloading? do 15 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 16 | plug Phoenix.LiveReloader 17 | plug Phoenix.CodeReloader 18 | end 19 | 20 | plug Plug.RequestId 21 | plug Plug.Logger 22 | 23 | plug Plug.Parsers, 24 | parsers: [:urlencoded, :multipart, :json], 25 | pass: ["*/*"], 26 | json_decoder: Poison 27 | 28 | plug Plug.MethodOverride 29 | plug Plug.Head 30 | 31 | # The session will be stored in the cookie and signed, 32 | # this means its contents can be read but not tampered with. 33 | # Set :encryption_salt if you would also like to encrypt it. 34 | plug Plug.Session, 35 | store: :cookie, 36 | key: "_tech_for_good_near_you_key", 37 | signing_salt: "iFrN+WPS" 38 | 39 | plug TechForGoodNearYou.Web.Router 40 | 41 | @doc """ 42 | Callback invoked for dynamically configuring the endpoint. 43 | 44 | It receives the endpoint configuration and checks if 45 | configuration should be loaded from the system environment. 46 | """ 47 | def init(_key, config) do 48 | if config[:load_from_system_env] do 49 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 50 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 51 | else 52 | {:ok, config} 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import TechForGoodNearYou.Web.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :tech_for_good_near_you 24 | end 25 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/plugs/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.Auth do 2 | import Plug.Conn 3 | import Phoenix.Controller 4 | import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] 5 | 6 | alias TechForGoodNearYou.Accounts 7 | alias TechForGoodNearYou.Accounts.Admin 8 | alias TechForGoodNearYou.Web.Router.Helpers 9 | 10 | def init(opts) do 11 | opts 12 | end 13 | 14 | def call(%{assigns: %{admin: %Admin{}}} = conn, _opts), do: conn 15 | 16 | def call(conn, _opts) do 17 | id = get_session(conn, :id) 18 | if admin = (id && Accounts.get_admin(id)) do 19 | assign(conn, :admin, admin) 20 | else 21 | assign(conn, :admin, nil) 22 | end 23 | end 24 | 25 | def login(conn, admin) do 26 | conn 27 | |> assign(:admin, admin) 28 | |> put_session(:id, admin.id) 29 | |> configure_session(renew: true) 30 | end 31 | 32 | def logout(conn) do 33 | configure_session(conn, drop: true) 34 | end 35 | 36 | def authenticate_admin(conn, _opts) do 37 | if conn.assigns.admin do 38 | conn 39 | else 40 | conn 41 | |> put_flash(:error, "You must be logged in to access that page") 42 | |> redirect(to: Helpers.session_path(conn, :new)) 43 | end 44 | end 45 | 46 | def login_by_username_and_pass(conn, username, given_pass) do 47 | admin = Accounts.get_admin_by(username: username) 48 | 49 | cond do 50 | admin && checkpw(given_pass, admin.password) -> 51 | {:ok, login(conn, admin)} 52 | admin -> 53 | {:error, :unauthorized, conn} 54 | true -> 55 | dummy_checkpw() 56 | {:error, :not_found, conn} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/requests/lat_lon.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.LatLon do 2 | 3 | def get_lat_lon(""), do: {:ok, %{}} 4 | 5 | def get_lat_lon(postcode) do 6 | case HTTPoison.get(request_url(postcode)) do 7 | {:ok, response} -> {:ok, grab_lat_lon(response)} 8 | {:error, reason} -> {:error, reason} 9 | end 10 | end 11 | 12 | defp request_url(postcode), do: "https://api.postcodes.io/postcodes/#{String.replace(postcode, " ", "")}" 13 | 14 | defp grab_lat_lon(response) do 15 | response 16 | |> Map.get(:body) 17 | |> Poison.decode! 18 | |> Map.get("result") 19 | |> Map.take(["latitude", "longitude"]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.Router do 2 | use TechForGoodNearYou.Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | plug TechForGoodNearYou.Web.Auth 11 | end 12 | 13 | pipeline :admin_layout do 14 | plug :put_layout, {TechForGoodNearYou.Web.LayoutView, :admin} 15 | end 16 | 17 | pipeline :api do 18 | plug :accepts, ["json"] 19 | end 20 | 21 | scope "/", TechForGoodNearYou.Web do 22 | pipe_through :browser 23 | 24 | get "/", ElmController, :index 25 | end 26 | 27 | scope "/user-event", TechForGoodNearYou.Web do 28 | pipe_through [:browser, :admin_layout] 29 | 30 | get "/new",EventController, :user_event_new 31 | post "/create", EventController, :user_event_create 32 | get "/confirmation", EventController, :user_event_confirmation 33 | end 34 | 35 | scope "/login", TechForGoodNearYou.Web do 36 | pipe_through [:browser, :admin_layout] 37 | 38 | get "/", SessionController, :new 39 | resources "/", SessionController, only: [:new, :create, :delete] 40 | end 41 | 42 | scope "/admin", TechForGoodNearYou.Web do 43 | pipe_through [:browser, :admin_layout, :authenticate_admin] 44 | 45 | resources "/events", EventController 46 | put "/approve-event/:id", EventController, :approve_event 47 | end 48 | 49 | scope "/api", TechForGoodNearYou.Web do 50 | pipe_through :api 51 | 52 | get "/meetup-events", MeetupController, :index 53 | get "/custom-events", EventController, :custom_events 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/elm/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/confirmation.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

We have received your event!

3 | 4 |

Thanks for your submission :)

5 |

Once your event has been approved by one of our admin it will be listed on Tech for Good Near You.

6 | 7 |
<%= link "Go back to event listings", to: elm_path(@conn, :index), class: "no-underline green hover-gold" %>
8 |
9 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/edit.html.eex: -------------------------------------------------------------------------------- 1 |

Edit Event

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: event_path(@conn, :update, @event) %> 5 | 6 | <%= link "Back", to: event_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 | <%= for field <- form_fields() do %> 9 |
10 | <%= label f, field, class: "w-40 w-10-ns tc"%> 11 | <%= text_input f, field, class: "w-60 w-40-ns mh2 outline-0 b--green ba pv2 br2 tc"%> 12 | <%= error_tag f, field%> 13 |
14 | <% end %> 15 | 16 |
17 | <%= datetime_select f, :time, builder: fn b -> %> 18 |
19 |
Time
20 |
21 | <%= b.(:hour, [class: "bn w3 f5 outline-0 green"]) %> : <%= b.(:minute, [class: "bn w3 f5 outline-0 green"]) %> 22 |
23 |
24 |
25 |
Date
26 |
27 | <%= b.(:day, [class: "bn w3 f5 outline-0 green"])%> / <%= b.(:month, [class: "bn w4 f5 outline-0 green"]) %> / <%= b.(:year, [class: "bn w3 f5 outline-0 green"]) %> 28 |
29 |
30 | <% end %> 31 | <%= error_tag f, :time%> 32 |
33 | 34 |
35 | <%= submit "Submit", class: "ba b--green pv2 ph5 bg-green white br2 f3 outline-0 pointer"%> 36 |
37 | <% end %> 38 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/index.html.eex: -------------------------------------------------------------------------------- 1 |

Events

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for event <- @approved_events do %> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | <% end %> 35 | 36 |
NameUrlTimeAddressPostcodeVenue nameGroup name
<%= event.name %><%= event.url %><%= event.time %><%= event.address %><%= event.postcode %><%= event.venue_name %><%= event.group_name %> 29 | <%= link "Show", to: event_path(@conn, :show, event)%> 30 | <%= link "Edit", to: event_path(@conn, :edit, event)%> 31 | <%= link "Delete", to: event_path(@conn, :delete, event), method: :delete, data: [confirm: "Are you sure?"]%> 32 |
37 | 38 | <%= link "Add event", to: event_path(@conn, :new) %> 39 | 40 |

Events awaiting approval

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | <%= for event <- @events_waiting_approval do %> 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | <% end %> 75 | 76 |
NameUrlTimeAddressPostcodeVenue nameGroup name
<%= event.name %><%= event.url %><%= event.time %><%= event.address %><%= event.postcode %><%= event.venue_name %><%= event.group_name %> 68 | <%= link "Show", to: event_path(@conn, :show, event)%> 69 | <%= link "Edit", to: event_path(@conn, :edit, event)%> 70 | <%= link "Delete", to: event_path(@conn, :delete, event), method: :delete, data: [confirm: "Are you sure?"]%> 71 | <%= link "Approve", to: event_path(@conn, :approve_event, event), method: :put, data: [confirm: "Are you sure?"]%> 72 |
77 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/new.html.eex: -------------------------------------------------------------------------------- 1 |

New Event

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: event_path(@conn, :create) %> 5 | 6 | <%= link "Back", to: event_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show Event

2 | 3 | 41 | 42 | <%= link "Edit", to: event_path(@conn, :edit, @event) %> 43 | <%= link "Back", to: event_path(@conn, :index) %> 44 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/event/user_event.html.eex: -------------------------------------------------------------------------------- 1 |
<%= link "Back to events", to: elm_path(@conn, :index), class: "no-underline green hover-gold" %>
2 |

Submit your event

3 | 4 | <%= render "form.html", changeset: @changeset, 5 | action: event_path(@conn, :user_event_create) %> 6 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/layout/admin.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Tech for Good Near You! 12 | "> 13 | 14 | 15 | 16 | 17 | 18 |
19 | <%= if @admin do %> 20 | <%= link "Log out", to: session_path(@conn, :delete, @admin), method: "delete" %> 21 | <% end %> 22 |
23 |
24 |
25 | <%= render @view_module, @view_template, assigns %> 26 |
27 |
28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Tech for Good Near You! 12 | "> 13 | 14 | 15 | 16 | <%= render @view_module, @view_template, assigns %> 17 | 22 | 23 | 24 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/templates/session/new.html.eex: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 | <%= form_for @conn, session_path(@conn, :create), [as: :session], fn f -> %> 4 |
5 | <%= text_input f, :username, placeholder: "username"%> 6 |
7 |
8 | <%= password_input f, :password, placeholder: "password", class: "form_control" %> 9 |
10 | <%= submit "Log in" %> 11 | <%= end %> 12 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/elm_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ElmView do 2 | use TechForGoodNearYou.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(TechForGoodNearYou.Web.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(TechForGoodNearYou.Web.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ErrorView do 2 | use TechForGoodNearYou.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/event_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.EventView do 2 | use TechForGoodNearYou.Web, :view 3 | alias TechForGoodNearYou.Web.EventView 4 | 5 | def render("events.json", %{events: events}) do 6 | %{data: render_many(events, EventView, "event.json")} 7 | end 8 | 9 | def render("event.json", %{event: event}) do 10 | %{name: event.name, 11 | url: event.url, 12 | time: event.time, 13 | address: event.address, 14 | latitude: event.latitude, 15 | longitude: event.longitude, 16 | group_name: event.group_name, 17 | venue_name: event.venue_name} 18 | end 19 | 20 | def form_fields, do: [:name, :url, :address, :postcode, :venue_name, :group_name] 21 | end 22 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.LayoutView do 2 | use TechForGoodNearYou.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.SessionView do 2 | use TechForGoodNearYou.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/tech_for_good_near_you/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use TechForGoodNearYou.Web, :controller 9 | use TechForGoodNearYou.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def controller do 20 | quote do 21 | use Phoenix.Controller, namespace: TechForGoodNearYou.Web 22 | import Plug.Conn 23 | import TechForGoodNearYou.Web.Router.Helpers 24 | import TechForGoodNearYou.Web.Gettext 25 | end 26 | end 27 | 28 | def view do 29 | quote do 30 | use Phoenix.View, root: "lib/tech_for_good_near_you/web/templates", 31 | namespace: TechForGoodNearYou.Web 32 | 33 | # Import convenience functions from controllers 34 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 35 | 36 | # Use all HTML functionality (forms, tags, etc) 37 | use Phoenix.HTML 38 | 39 | import TechForGoodNearYou.Web.Router.Helpers 40 | import TechForGoodNearYou.Web.ErrorHelpers 41 | import TechForGoodNearYou.Web.Gettext 42 | end 43 | end 44 | 45 | def router do 46 | quote do 47 | use Phoenix.Router 48 | import Plug.Conn 49 | import Phoenix.Controller 50 | import TechForGoodNearYou.Web.Auth, only: [authenticate_admin: 2] 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import TechForGoodNearYou.Web.Gettext 58 | end 59 | end 60 | 61 | @doc """ 62 | When used, dispatch to the appropriate controller/view/etc. 63 | """ 64 | defmacro __using__(which) when is_atom(which) do 65 | apply(__MODULE__, which, []) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :tech_for_good_near_you, 6 | version: "0.0.1", 7 | elixir: "~> 1.4", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | start_permanent: Mix.env == :prod, 11 | aliases: aliases(), 12 | deps: deps()] 13 | end 14 | 15 | # Configuration for the OTP application. 16 | # 17 | # Type `mix help compile.app` for more information. 18 | def application do 19 | [mod: {TechForGoodNearYou.Application, []}, 20 | extra_applications: [:logger, :runtime_tools, :httpoison, :comeonin]] 21 | end 22 | 23 | # Specifies which paths to compile per environment. 24 | defp elixirc_paths(:test), do: ["lib", "test/support"] 25 | defp elixirc_paths(_), do: ["lib"] 26 | 27 | # Specifies your project dependencies. 28 | # 29 | # Type `mix help deps` for examples and options. 30 | defp deps do 31 | [{:phoenix, "~> 1.3.0-rc"}, 32 | {:phoenix_pubsub, "~> 1.0"}, 33 | {:phoenix_ecto, "~> 3.2"}, 34 | {:postgrex, ">= 0.0.0"}, 35 | {:phoenix_html, "~> 2.6"}, 36 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 37 | {:gettext, "~> 0.11"}, 38 | {:cowboy, "~> 1.0"}, 39 | {:httpoison, "~> 0.11.2"}, 40 | {:comeonin, "~> 3.1.0"}] 41 | end 42 | 43 | # Aliases are shortcuts or tasks specific to the current project. 44 | # For example, to create, migrate and run the seeds file at once: 45 | # 46 | # $ mix ecto.setup 47 | # 48 | # See the documentation for `Mix` for more info on aliases. 49 | defp aliases do 50 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 51 | "ecto.reset": ["ecto.drop", "ecto.setup"], 52 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], []}, 2 | "comeonin": {:hex, :comeonin, "3.1.0", "fbf18d43a7cfe7edebaa3bcf702d9a78821d3d3ed0c57c65419f99a8816dfaee", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, optional: false]}]}, 3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 4 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]}, 5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 6 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 7 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, 8 | "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 9 | "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], []}, 10 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 11 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, 12 | "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, optional: false]}, {:idna, "5.0.2", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 13 | "httpoison": {:hex, :httpoison, "0.11.2", "9e59f17a473ef6948f63c51db07320477bad8ba88cf1df60a3eee01150306665", [:mix], [{:hackney, "~> 1.8.0", [hex: :hackney, optional: false]}]}, 14 | "httpotion": {:hex, :httpotion, "3.0.2", "525b9bfeb592c914a61a8ee31fdde3871e1861dfe805f8ee5f711f9f11a93483", [:mix], [{:ibrowse, "~> 4.2", [hex: :ibrowse, optional: false]}]}, 15 | "ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [:rebar3], []}, 16 | "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, optional: false]}]}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 18 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, 19 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 20 | "phoenix": {:hex, :phoenix, "1.3.0-rc.2", "53104ada25ba85fe160268c0dc826fe038bc074293730b4522fb9aca28d8aa13", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]}, 21 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.2.3", "450c749876ff1de4a78fdb305a142a76817c77a1cd79aeca29e5fc9a6c630b26", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 22 | "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 23 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, 24 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], []}, 25 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 26 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, 27 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 28 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 29 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}, 30 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, 31 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], []}} 32 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | compile="compile_static.sh" 2 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170620103715_create_meet_ups_event.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo.Migrations.CreateTechForGoodNearYou.MeetUps.Event do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:meet_ups_events) do 6 | add :name, :string, null: false 7 | add :time, :naive_datetime, null: false 8 | add :address, :string, null: false 9 | add :postcode, :string, null: false 10 | add :venue_name, :string 11 | add :group_name, :string 12 | 13 | timestamps() 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170620113852_add_url_column_to_events.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo.Migrations.AddUrlColumnToEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:meet_ups_events) do 6 | add :url, :string, null: false 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170620150507_add_lat_lon_to_events.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo.Migrations.AddLatLonToEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:meet_ups_events) do 6 | add :latitude, :float 7 | add :longitude, :float 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170623112555_create_accounts_admin.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo.Migrations.CreateTechForGoodNearYou.Accounts.Admin do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:accounts_admins) do 6 | add :username, :string 7 | add :password, :string 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170704091944_add_approved_column_to_events.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Repo.Migrations.AddApprovedColumnToEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:meet_ups_events) do 6 | add :approved, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # TechForGoodNearYou.Repo.insert!(%TechForGoodNearYou.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/elm/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Radius 4 | import Test.Runner.Node exposing (run, TestProgram) 5 | import Json.Encode exposing (Value) 6 | 7 | 8 | main : TestProgram 9 | main = 10 | run emit Radius.suite 11 | 12 | 13 | port emit : ( String, Value ) -> Cmd msg 14 | -------------------------------------------------------------------------------- /test/elm/Radius.elm: -------------------------------------------------------------------------------- 1 | module Radius exposing (..) 2 | 3 | import Test exposing (..) 4 | import Expect 5 | import Data.Location.Radius exposing (..) 6 | import Types exposing (..) 7 | 8 | 9 | suite : Test 10 | suite = 11 | describe "latLngToMiles should" 12 | [ test "should convert two pairs of lat lngs to a distance in miles" <| 13 | \_ -> 14 | let 15 | c1 = 16 | Coords 51.464967 0.127023 17 | 18 | c2 = 19 | Coords 51.529902 0.042044 20 | 21 | actual = 22 | latLngToMiles c1 c2 23 | in 24 | Expect.equal (round actual) 6 25 | ] 26 | -------------------------------------------------------------------------------- /test/elm/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Sample Elm Test", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../app/elm" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 14 | "mgold/elm-random-pcg": "4.0.2 <= v < 5.0.0", 15 | "elm-community/elm-test": "3.0.0 <= v < 4.0.0", 16 | "rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0", 17 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 18 | "ccapndave/elm-update-extra": "2.3.1 <= v < 3.0.0", 19 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 20 | "elm-lang/geolocation": "1.0.2 <= v < 2.0.0", 21 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 22 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 23 | "justinmimbs/elm-date-extra": "2.0.3 <= v < 3.0.0" 24 | }, 25 | "elm-version": "0.18.0 <= v < 0.19.0" 26 | } 27 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint TechForGoodNearYou.Web.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TechForGoodNearYou.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(TechForGoodNearYou.Repo, {:shared, self()}) 33 | end 34 | :ok 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import TechForGoodNearYou.Web.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint TechForGoodNearYou.Web.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TechForGoodNearYou.Repo) 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(TechForGoodNearYou.Repo, {:shared, self()}) 34 | end 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias TechForGoodNearYou.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import TechForGoodNearYou.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TechForGoodNearYou.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(TechForGoodNearYou.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/meet_ups/meet_ups_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.MeetUpsTest do 2 | use TechForGoodNearYou.DataCase 3 | 4 | alias TechForGoodNearYou.MeetUps 5 | 6 | describe "events" do 7 | alias TechForGoodNearYou.MeetUps.Event 8 | 9 | @valid_attrs %{address: "some address", group_name: "some group_name", name: "some name", postcode: "sw99ng", url: "www.event.com", time: ~N[2010-04-17 14:00:00.000000], venue_name: "some venue_name"} 10 | @update_attrs %{address: "some updated address", group_name: "some updated group_name", name: "some updated name", postcode: "e20sy", url: "www.event.com", time: ~N[2011-05-18 15:01:01.000000], venue_name: "some updated venue_name"} 11 | @invalid_attrs %{address: nil, group_name: nil, name: nil, postcode: nil, time: nil, venue_name: nil} 12 | 13 | def event_fixture(attrs \\ %{}) do 14 | {:ok, event} = 15 | attrs 16 | |> Enum.into(@valid_attrs) 17 | |> MeetUps.create_event() 18 | 19 | event 20 | end 21 | 22 | test "list_events/0 returns all events" do 23 | event = event_fixture() 24 | assert MeetUps.list_events() == [event] 25 | end 26 | 27 | test "get_event!/1 returns the event with given id" do 28 | event = event_fixture() 29 | assert MeetUps.get_event!(event.id) == event 30 | end 31 | 32 | test "create_event/1 with valid data creates a event" do 33 | assert {:ok, %Event{} = event} = MeetUps.create_event(@valid_attrs) 34 | assert event.address == "some address" 35 | assert event.group_name == "some group_name" 36 | assert event.name == "some name" 37 | assert event.postcode == "sw99ng" 38 | assert event.time == ~N[2010-04-17 14:00:00.000000] 39 | assert event.venue_name == "some venue_name" 40 | end 41 | 42 | test "create_event/1 with invalid data returns error changeset" do 43 | assert {:error, %Ecto.Changeset{}} = MeetUps.create_event(@invalid_attrs) 44 | end 45 | 46 | test "update_event/2 with valid data updates the event" do 47 | event = event_fixture() 48 | assert {:ok, event} = MeetUps.update_event(event, @update_attrs) 49 | assert %Event{} = event 50 | assert event.address == "some updated address" 51 | assert event.group_name == "some updated group_name" 52 | assert event.name == "some updated name" 53 | assert event.postcode == "e20sy" 54 | assert event.time == ~N[2011-05-18 15:01:01.000000] 55 | assert event.venue_name == "some updated venue_name" 56 | end 57 | 58 | test "update_event/2 with invalid data returns error changeset" do 59 | event = event_fixture() 60 | assert {:error, %Ecto.Changeset{}} = MeetUps.update_event(event, @invalid_attrs) 61 | assert event == MeetUps.get_event!(event.id) 62 | end 63 | 64 | test "delete_event/1 deletes the event" do 65 | event = event_fixture() 66 | assert {:ok, %Event{}} = MeetUps.delete_event(event) 67 | assert_raise Ecto.NoResultsError, fn -> MeetUps.get_event!(event.id) end 68 | end 69 | 70 | test "change_event/1 returns a event changeset" do 71 | event = event_fixture() 72 | assert %Ecto.Changeset{} = MeetUps.change_event(event) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/web/controllers/elm_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ElmControllerTest do 2 | use TechForGoodNearYou.Web.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Tech for Good Near You!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/web/controllers/event_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.EventControllerTest do 2 | use TechForGoodNearYou.Web.ConnCase 3 | 4 | alias TechForGoodNearYou.MeetUps 5 | 6 | @create_attrs %{address: "some address", group_name: "some group_name", name: "some name", postcode: "sw99ng", url: "www.event.com", time: %DateTime{calendar: Calendar.ISO, day: 17, hour: 14, microsecond: {0, 6}, minute: 0, month: 4, second: 0, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0, year: 2010, zone_abbr: "UTC"}, venue_name: "some venue_name"} 7 | @update_attrs %{address: "some updated address", group_name: "some updated group_name", name: "some updated name", postcode: "e20sy", url: "www.event.com", time: %DateTime{calendar: Calendar.ISO, day: 18, hour: 15, microsecond: {0, 6}, minute: 1, month: 5, second: 1, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0, year: 2011, zone_abbr: "UTC"}, venue_name: "some updated venue_name"} 8 | @invalid_attrs %{address: nil, group_name: nil, name: nil, time: nil, venue_name: nil} 9 | 10 | def fixture(:event) do 11 | {:ok, event} = MeetUps.create_event(@create_attrs) 12 | event 13 | end 14 | 15 | test "lists all entries on index", %{conn: conn} do 16 | conn = get conn, event_path(conn, :index) 17 | assert html_response(conn, 200) =~ "Listing Events" 18 | end 19 | 20 | test "renders form for new events", %{conn: conn} do 21 | conn = get conn, event_path(conn, :new) 22 | assert html_response(conn, 200) =~ "New Event" 23 | end 24 | 25 | test "creates event and redirects to show when data is valid", %{conn: conn} do 26 | conn = post conn, event_path(conn, :create), event: @create_attrs 27 | 28 | assert %{id: id} = redirected_params(conn) 29 | assert redirected_to(conn) == event_path(conn, :show, id) 30 | 31 | conn = get conn, event_path(conn, :show, id) 32 | assert html_response(conn, 200) =~ "Show Event" 33 | end 34 | 35 | test "renders form for editing chosen event", %{conn: conn} do 36 | event = fixture(:event) 37 | conn = get conn, event_path(conn, :edit, event) 38 | assert html_response(conn, 200) =~ "Edit Event" 39 | end 40 | 41 | test "updates chosen event and redirects when data is valid", %{conn: conn} do 42 | event = fixture(:event) 43 | conn = put conn, event_path(conn, :update, event), event: @update_attrs 44 | assert redirected_to(conn) == event_path(conn, :show, event) 45 | 46 | conn = get conn, event_path(conn, :show, event) 47 | assert html_response(conn, 200) =~ "some updated address" 48 | end 49 | 50 | test "does not update chosen event and renders errors when data is invalid", %{conn: conn} do 51 | event = fixture(:event) 52 | conn = put conn, event_path(conn, :update, event), event: @invalid_attrs 53 | assert html_response(conn, 200) =~ "Edit Event" 54 | end 55 | 56 | test "deletes chosen event", %{conn: conn} do 57 | event = fixture(:event) 58 | conn = delete conn, event_path(conn, :delete, event) 59 | assert redirected_to(conn) == event_path(conn, :index) 60 | assert_error_sent 404, fn -> 61 | get conn, event_path(conn, :show, event) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/web/views/elm_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ElmViewTest do 2 | use TechForGoodNearYou.Web.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.ErrorViewTest do 2 | use TechForGoodNearYou.Web.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(TechForGoodNearYou.Web.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(TechForGoodNearYou.Web.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(TechForGoodNearYou.Web.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/tech_for_good_near_you/web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TechForGoodNearYou.Web.LayoutViewTest do 2 | use TechForGoodNearYou.Web.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(TechForGoodNearYou.Repo, :manual) 4 | 5 | --------------------------------------------------------------------------------