Skip to content

Commit acf4333

Browse files
committedNov 22, 2019
front-end: Add email notification preferences
This commit adds the front-end capabilities to coincide with the API endpoint that was built on the server. There's also a small but fixed in the api-token-row that's fixed.
1 parent 3dc6074 commit acf4333

File tree

9 files changed

+232
-2
lines changed

9 files changed

+232
-2
lines changed
 

‎app/components/api-token-row.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default Component.extend({
88

99
didInsertElement() {
1010
let input = this.element.querySelector('input');
11-
if (input.focus) {
11+
if (input && input.focus) {
1212
input.focus();
1313
}
1414
},

‎app/components/owned-crate-row.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Component from '@ember/component';
2+
import { computed } from '@ember/object';
3+
import { alias } from '@ember/object/computed';
4+
5+
export default Component.extend({
6+
tagName: 'li',
7+
8+
name: alias('ownedCrate.name'),
9+
controlId: computed('ownedCrate.id', function() {
10+
return `${this.ownedCrate.id}-email-notifications`;
11+
}),
12+
emailNotifications: alias('ownedCrate.email_notifications'),
13+
14+
actions: {
15+
toggleEmailNotifications() {
16+
this.set('emailNotifications', !this.get('emailNotifications'));
17+
},
18+
},
19+
});

‎app/controllers/me/index.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Controller from '@ember/controller';
2-
import { sort, filterBy, notEmpty } from '@ember/object/computed';
2+
import { alias, sort, filterBy, notEmpty } from '@ember/object/computed';
33
import { inject as service } from '@ember/service';
4+
import ajax from 'ember-fetch/ajax';
45

56
export default Controller.extend({
67
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
@@ -12,10 +13,38 @@ export default Controller.extend({
1213

1314
isResetting: false,
1415

16+
ownedCrates: alias('model.ownedCrates'),
17+
1518
newTokens: filterBy('model.api_tokens', 'isNew', true),
1619
disableCreate: notEmpty('newTokens'),
1720

21+
emailNotificationsError: false,
22+
emailNotificationsSuccess: false,
23+
1824
actions: {
25+
async saveEmailNotifications() {
26+
try {
27+
await ajax(`/api/v1/me/email_notifications`, {
28+
method: 'PUT',
29+
body: JSON.stringify(
30+
this.get('ownedCrates').map(c => ({
31+
id: parseInt(c.id, 10),
32+
email_notifications: c.email_notifications,
33+
})),
34+
),
35+
});
36+
this.setProperties({
37+
emailNotificationsError: false,
38+
emailNotificationsSuccess: true,
39+
});
40+
} catch (err) {
41+
console.error(err);
42+
this.setProperties({
43+
emailNotificationsError: true,
44+
emailNotificationsSuccess: false,
45+
});
46+
}
47+
},
1948
startNewToken() {
2049
this.store.createRecord('api-token', {
2150
created_at: new Date(Date.now() + 2000),

‎app/models/owned-crate.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import DS from 'ember-data';
2+
3+
export default DS.Model.extend({
4+
name: DS.attr('string'),
5+
email_notifications: DS.attr('boolean'),
6+
});

‎app/routes/me/index.js

+11
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ import Route from '@ember/routing/route';
33
import AuthenticatedRoute from '../../mixins/authenticated-route';
44

55
export default Route.extend(AuthenticatedRoute, {
6+
actions: {
7+
willTransition: function() {
8+
this.controller
9+
.setProperties({
10+
emailNotificationsSuccess: false,
11+
emailNotificationsError: false,
12+
})
13+
.clear();
14+
},
15+
},
616
model() {
717
return {
818
user: this.get('session.currentUser'),
19+
ownedCrates: this.get('session.ownedCrates'),
920
api_tokens: this.store.findAll('api-token'),
1021
};
1122
},

‎app/services/session.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { A } from '@ember/array';
12
import Service, { inject as service } from '@ember/service';
23
import ajax from 'ember-fetch/ajax';
34

@@ -7,6 +8,7 @@ export default Service.extend({
78
isLoggedIn: false,
89
currentUser: null,
910
currentUserDetected: false,
11+
ownedCrates: A(),
1012

1113
store: service(),
1214
router: service(),
@@ -66,6 +68,9 @@ export default Service.extend({
6668
fetchUser() {
6769
return ajax('/api/v1/me').then(response => {
6870
this.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
71+
this.ownedCrates.pushObjects(
72+
response.owned_crates.map(c => this.store.push(this.store.normalize('owned-crate', c))),
73+
);
6974
});
7075
},
7176

‎app/styles/me.scss

+116
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,122 @@
116116
}
117117
}
118118

119+
#me-email-notifications {
120+
border-bottom: 5px solid $gray-border;
121+
padding-bottom: 20px;
122+
margin-bottom: 20px;
123+
@include display-flex;
124+
@include flex-direction(column);
125+
126+
.row {
127+
@include display-flex;
128+
@include flex-direction(row);
129+
130+
.error, .success {
131+
border-top-width: 0px;
132+
font-weight: bold;
133+
134+
padding: 0px 10px 10px 20px;
135+
}
136+
137+
.error {
138+
color: rgb(216, 0, 41);
139+
}
140+
141+
.success {
142+
color: green;
143+
}
144+
}
145+
146+
ul {
147+
padding: 0;
148+
@include flex-grow(1);
149+
150+
li {
151+
background-color: #fff;
152+
display: block;
153+
position: relative;
154+
border: 1px solid #d5d3cb;
155+
}
156+
157+
label {
158+
padding: 20px 30px;
159+
width: 100%;
160+
display: block;
161+
text-align: left;
162+
font-weight: bold;
163+
cursor: pointer;
164+
position: relative;
165+
z-index: 2;
166+
transition: color 200ms ease-in;
167+
overflow: hidden;
168+
169+
&:before {
170+
width: 100%;
171+
height: 10px;
172+
border-radius: 50%;
173+
content: '';
174+
background-color: $main-bg-dark;
175+
position: absolute;
176+
left: 50%;
177+
top: 50%;
178+
transform: translate(-50%, -50%) scale3d(1, 1, 1);
179+
opacity: 0;
180+
z-index: -1;
181+
}
182+
183+
&:after {
184+
width: 32px;
185+
height: 32px;
186+
content: '';
187+
border: 2px solid #d5d3cb;
188+
border-radius: 50%;
189+
z-index: 2;
190+
position: absolute;
191+
right: 30px;
192+
top: 50%;
193+
transform: translateY(-50%);
194+
cursor: pointer;
195+
}
196+
}
197+
198+
input:checked ~ label {
199+
&:before {
200+
transform: translate(-50%, -50%) scale3d(56, 56, 1);
201+
opacity: 1;
202+
}
203+
204+
&:after {
205+
background-color: #cfc487;
206+
border-color: #cfc487;
207+
background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.414 11L4 12.414l5.414 5.414L20.828 6.414 19.414 5l-10 10z' fill='%23383838' fill-rule='nonzero'/%3E%3C/svg%3E ");
208+
background-repeat: no-repeat;
209+
background-position: 3px 4px;
210+
}
211+
}
212+
213+
input {
214+
width: 32px;
215+
height: 32px;
216+
order: 1;
217+
z-index: 2;
218+
position: absolute;
219+
right: 30px;
220+
top: 50%;
221+
transform: translateY(-50%);
222+
cursor: pointer;
223+
visibility: hidden;
224+
}
225+
}
226+
227+
.right {
228+
@include flex(2);
229+
@include display-flex;
230+
@include justify-content(flex-end);
231+
@include align-self(center);
232+
}
233+
}
234+
119235
#me-api {
120236
@media only screen and (max-width: 350px) {
121237
.api { display: none; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{input
2+
type="checkbox"
3+
name=controlId
4+
id=controlId
5+
checked=emailNotifications
6+
change=(action "toggleEmailNotifications")
7+
class="form-control"
8+
}}
9+
<label for="{{ controlId }}">
10+
{{ name }}
11+
</label>

‎app/templates/me/index.hbs

+33
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@
2525
{{email-input type='email' value=model.user.email user=model.user}}
2626
</div>
2727

28+
<form id='me-email-notifications' {{ action 'saveEmailNotifications' on='submit' }} >
29+
<h2>Email Notification Preferences</h2>
30+
31+
<p>
32+
To aid detection of unauthorized crate changes, we email you each time a new version of a crate you own is pushed.
33+
By receiving and reading these emails, you help protect the Rust ecosystem.
34+
</p>
35+
36+
<div class='row'>
37+
<ul>
38+
{{#each ownedCrates as |ownedCrate|}}
39+
{{owned-crate-row ownedCrate=ownedCrate}}
40+
{{/each}}
41+
</ul>
42+
</div>
43+
44+
<div class='row'>
45+
{{#if emailNotificationsError}}
46+
<div class='error'>
47+
An error occurred while saving your email preferences.
48+
</div>
49+
{{/if}}
50+
{{#if emailNotificationsSuccess}}
51+
<div class='success'>
52+
Your email notification preferences have been updated!
53+
</div>
54+
{{/if}}
55+
<div class='right'>
56+
<button type='submit' class='yellow-button'>Update</button>
57+
</div>
58+
</div>
59+
</form>
60+
2861
<div id='me-api'>
2962
<div class='me-subheading'>
3063
<h2>API Access</h2>

0 commit comments

Comments
 (0)
Please sign in to comment.