Woodshop
Search…
Create a custom tinder-like validation view
This example shows you how you can implement a time-saving profile validation view using keyboard keys to trigger approve/reject actions.
In our example, we want to Approve or Reject new customers profiles and more specifically:
    We want to preview informations from the user's profile
    We want to approve a customer by pressing the ArrowRight key
    We want to reject a customer by pressing the ArrowLeft key

How it works

Models definition

Here is the definition of the underlying model for this view
1
module.exports = (sequelize, DataTypes) => {
2
const { Sequelize } = sequelize;
3
const customerValidations = sequelize.define('customerValidations', {
4
firstname: {
5
type: DataTypes.STRING,
6
},
7
lastname: {
8
type: DataTypes.STRING,
9
},
10
email: {
11
type: DataTypes.STRING,
12
},
13
createdAt: {
14
type: DataTypes.DATE,
15
},
16
status: {
17
type: DataTypes.STRING,
18
},
19
avatar: {
20
type: DataTypes.STRING,
21
},
22
}, {
23
tableName: 'customers',
24
underscored: true,
25
schema: process.env.DATABASE_SCHEMA,
26
});
27
28
29
return customerValidations;
30
};
31
Copied!

Smart view definition

Learn more about smart views. This file contains the HTML, JS and CSS needed to build the view.
Template
Component
Style
1
<div class="c-smart-view">
2
<div class="c-smart-view__content">
3
{{#if (eq @recordsCount 0)}}
4
<span class="c-smart-view_icon fa fa-{{@collection.iconOrDefault}} fa-5x"></span>
5
<h1>
6
{{@collection.pluralizedDisplayName}}
7
</h1>
8
<p>
9
There are no items to process.
10
</p>
11
{{/if}}
12
{{#unless (eq @recordsCount 0)}}
13
<div class="wrapper-view" {{did-insert this.setDefaultCurrentRecord}}>
14
<div class="wrapper-list">
15
{{#each @records as |record|}}
16
<div class="list--item align-left {{if (eq @records.firstObject record) 'selected'}}">
17
<div class="list--item__values">
18
<h3><span>name :</span> {{record.forest-firstname}} {{record.forest-lastname}}</h3>
19
<p><span>email :</span> {{record.forest-email}}</p>
20
<p>{{moment-format record.forest-createdAt 'LLL'}}</p>
21
</div>
22
</div>
23
{{/each}}
24
</div>
25
<div class="wrapper-content">
26
<h1>
27
{{@recordsCount}} items to process
28
</h1>
29
<p>
30
Press <i class="fa fa-arrow-right"></i> to approve
31
</p>
32
<p>
33
Press <i class="fa fa-arrow-left"></i> to reject
34
</p>
35
<div class="record-container" >
36
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
37
<div class= "c-beta-label__label">Name</div>
38
<p class="c-row-value align-left">{{@records.firstObject.forest-firstname}} {{@records.firstObject.forest-lastname}}</p>
39
</div>
40
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
41
<div class= "c-beta-label__label">Email</div>
42
<p class="c-row-value align-left">{{@records.firstObject.forest-email}}</p>
43
</div>
44
<div class= "row-value-image">
45
<img src="{{@records.firstObject.forest-avatar}}" width="300" height="400">
46
</div>
47
<div class="c-beta-button c-beta-button--secondary" onclick={{ action 'triggerSmartAction' @collection 'reject' @records.firstObject}}>Reject</div>
48
<div class="c-beta-button c-beta-button--primary" onclick={{ action 'triggerSmartAction' @collection 'approve' @records.firstObject}}>Approve</div>
49
</div>
50
</div>
51
</div>
52
{{/unless}}
53
</div>
54
</div>
Copied!
1
import Component from '@glimmer/component';
2
import { action, computed } from '@ember/object';
3
import { triggerSmartAction, deleteRecords, getCollectionId, loadExternalStyle, loadExternalJavascript } from 'client/utils/smart-view-utils';
4
5
export default class TinderView extends Component {
6
7
constructor(...args) {
8
super(...args);
9
this.eventListener = this._eventListener.bind(this);
10
document.addEventListener("keydown", this.eventListener);
11
}
12
13
@action
14
triggerSmartAction(...args) {
15
return triggerSmartAction(this, ...args);
16
}
17
18
_eventListener(event) {
19
const keyCode = event.key;
20
if (['ArrowLeft', 'ArrowRight'].includes(keyCode)) {
21
event.stopPropagation();
22
event.preventDefault();
23
this.triggerSmartAction(this.args.collection, (keyCode === 'ArrowLeft') ? 'reject' : 'approve', this.args.records.firstObject);
24
}
25
}
26
27
willDestroy() {
28
document.removeEventListener('keydown', this.eventListener);
29
}
30
}
31
Copied!
1
.c-smart-view {
2
display: flex;
3
white-space: normal;
4
position: absolute;
5
bottom: 0;
6
left: 0;
7
right: 0;
8
top: 0;
9
background-color: var(--color-beta-surface);
10
}
11
12
.c-smart-view__content {
13
margin: auto;
14
text-align: center;
15
color: var(--color-beta-on-surface_medium);
16
}
17
18
.c-smart-view_icon {
19
margin-bottom: 32px;
20
}
21
22
.record-container {
23
border-color: var(--color-beta-on-surface_border);
24
border-style: solid;
25
border-width: 1px;
26
padding: 40px;
27
margin: 20px 0px;
28
background-color: var(--color-beta-surface-accent);
29
}
30
31
.row-value-image {
32
margin-bottom: 15px;
33
border-color: var(--color-beta-on-surface_border);
34
border-style: solid;
35
border-width: 1px;
36
}
37
38
.align-left {
39
text-align: left;
40
}
41
42
.wrapper-view {
43
display: flex;
44
width: 100vw;
45
height:100vh;
46
}
47
48
.wrapper-list {
49
min-width: 330px;
50
border: 1px solid var(--color-beta-on-surface_border);
51
border-left: 0px;
52
overflow: scroll;
53
}
54
55
.wrapper-content {
56
margin-left: 15vw;
57
padding:50px;
58
height:100vh;
59
}
60
61
.selected {
62
border-left: 8px solid var(--color-beta-on-surface_border);
63
background-color: var(--color-surface-accent);
64
padding-right: Opx;
65
}
66
67
.list--item {
68
border-bottom: 1px solid #D9DDE1;
69
padding: 14px 30px;
70
}
71
72
.list--item__values h3 {
73
font-size: 14px;
74
font-weight: bold;
75
padding: 2px 0;
76
}
77
.list--item__values h3 span {
78
font-size: 10px;
79
text-transform: uppercase;
80
}
81
.list--item__values p {
82
font-size: 10px;
83
padding: 2px 0;
84
}
85
.list--item__values p span {
86
text-transform: uppercase;
87
}
Copied!
Last modified 7mo ago