├── .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 |
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 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/calendar.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/chevron.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/clock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/crosshair-white.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/crosshair.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/static/images/group.svg:
--------------------------------------------------------------------------------
1 |
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 | | Name |
7 | Url |
8 | Time |
9 | Address |
10 | Postcode |
11 | Venue name |
12 | Group name |
13 |
14 | |
15 |
16 |
17 |
18 | <%= for event <- @approved_events do %>
19 |
20 | | <%= event.name %> |
21 | <%= event.url %> |
22 | <%= event.time %> |
23 | <%= event.address %> |
24 | <%= event.postcode %> |
25 | <%= event.venue_name %> |
26 | <%= event.group_name %> |
27 |
28 |
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 | |
33 |
34 | <% end %>
35 |
36 |
37 |
38 | <%= link "Add event", to: event_path(@conn, :new) %>
39 |
40 | Events awaiting approval
41 |
42 |
43 |
44 |
45 | | Name |
46 | Url |
47 | Time |
48 | Address |
49 | Postcode |
50 | Venue name |
51 | Group name |
52 |
53 | |
54 |
55 |
56 |
57 | <%= for event <- @events_waiting_approval do %>
58 |
59 | | <%= event.name %> |
60 | <%= event.url %> |
61 | <%= event.time %> |
62 | <%= event.address %> |
63 | <%= event.postcode %> |
64 | <%= event.venue_name %> |
65 | <%= event.group_name %> |
66 |
67 |
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 | |
73 |
74 | <% end %>
75 |
76 |
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 |
4 |
5 | -
6 | Name:
7 | <%= @event.name %>
8 |
9 |
10 | -
11 | Url:
12 | <%= @event.url %>
13 |
14 |
15 | -
16 | Time:
17 | <%= @event.time %>
18 |
19 |
20 | -
21 | Address:
22 | <%= @event.address %>
23 |
24 |
25 | -
26 | Postcode:
27 | <%= @event.postcode %>
28 |
29 |
30 | -
31 | Venue name:
32 | <%= @event.venue_name %>
33 |
34 |
35 | -
36 | Group name:
37 | <%= @event.group_name %>
38 |
39 |
40 |
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 | <%= get_flash(@conn, :info) %>
17 | <%= get_flash(@conn, :error) %>
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 |
--------------------------------------------------------------------------------