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

module.exports = (sequelize, DataTypes) => {
const { Sequelize } = sequelize;
const customerValidations = sequelize.define('customerValidations', {
firstname: {
type: DataTypes.STRING,
},
lastname: {
type: DataTypes.STRING,
},
email: {
type: DataTypes.STRING,
},
createdAt: {
type: DataTypes.DATE,
},
status: {
type: DataTypes.STRING,
},
avatar: {
type: DataTypes.STRING,
},
}, {
tableName: 'customers',
underscored: true,
schema: process.env.DATABASE_SCHEMA,
});
return customerValidations;
};

Smart view definition

Learn more about smart views. This file contains the HTML, JS and CSS needed to build the view.

Template
Component
Style
Template
<div class="c-smart-view">
<div class="c-smart-view__content">
{{#if (eq @recordsCount 0)}}
<span class="c-smart-view_icon fa fa-{{@collection.iconOrDefault}} fa-5x"></span>
<h1>
{{@collection.pluralizedDisplayName}}
</h1>
<p>
There are no items to process.
</p>
{{/if}}
{{#unless (eq @recordsCount 0)}}
<div class="wrapper-view" {{did-insert this.setDefaultCurrentRecord}}>
<div class="wrapper-list">
{{#each @records as |record|}}
<div class="list--item align-left {{if (eq @records.firstObject record) 'selected'}}">
<div class="list--item__values">
<h3><span>name :</span> {{record.forest-firstname}} {{record.forest-lastname}}</h3>
<p><span>email :</span> {{record.forest-email}}</p>
<p>{{moment-format record.forest-createdAt 'LLL'}}</p>
</div>
</div>
{{/each}}
</div>
<div class="wrapper-content">
<h1>
{{@recordsCount}} items to process
</h1>
<p>
Press <i class="fa fa-arrow-right"></i> to approve
</p>
<p>
Press <i class="fa fa-arrow-left"></i> to reject
</p>
<div class="record-container" >
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
<div class= "c-beta-label__label">Name</div>
<p class="c-row-value align-left">{{@records.firstObject.forest-firstname}} {{@records.firstObject.forest-lastname}}</p>
</div>
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
<div class= "c-beta-label__label">Email</div>
<p class="c-row-value align-left">{{@records.firstObject.forest-email}}</p>
</div>
<div class= "row-value-image">
<img src="{{@records.firstObject.forest-avatar}}" width="300" height="400">
</div>
<div class="c-beta-button c-beta-button--secondary" onclick={{ action 'triggerSmartAction' @collection 'reject' @records.firstObject}}>Reject</div>
<div class="c-beta-button c-beta-button--primary" onclick={{ action 'triggerSmartAction' @collection 'approve' @records.firstObject}}>Approve</div>
</div>
</div>
</div>
{{/unless}}
</div>
</div>
Component
import Component from '@glimmer/component';
import { action, computed } from '@ember/object';
import { triggerSmartAction, deleteRecords, getCollectionId, loadExternalStyle, loadExternalJavascript } from 'client/utils/smart-view-utils';
export default class TinderView extends Component {
constructor(...args) {
super(...args);
this.eventListener = this._eventListener.bind(this);
document.addEventListener("keydown", this.eventListener);
}
@action
triggerSmartAction(...args) {
return triggerSmartAction(this, ...args);
}
_eventListener(event) {
const keyCode = event.key;
if (['ArrowLeft', 'ArrowRight'].includes(keyCode)) {
event.stopPropagation();
event.preventDefault();
this.triggerSmartAction(this.args.collection, (keyCode === 'ArrowLeft') ? 'reject' : 'approve', this.args.records.firstObject);
}
}
willDestroy() {
document.removeEventListener('keydown', this.eventListener);
}
}
Style
.c-smart-view {
display: flex;
white-space: normal;
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
background-color: var(--color-beta-surface);
}
.c-smart-view__content {
margin: auto;
text-align: center;
color: var(--color-beta-on-surface_medium);
}
.c-smart-view_icon {
margin-bottom: 32px;
}
.record-container {
border-color: var(--color-beta-on-surface_border);
border-style: solid;
border-width: 1px;
padding: 40px;
margin: 20px 0px;
background-color: var(--color-beta-surface-accent);
}
.row-value-image {
margin-bottom: 15px;
border-color: var(--color-beta-on-surface_border);
border-style: solid;
border-width: 1px;
}
.align-left {
text-align: left;
}
.wrapper-view {
display: flex;
width: 100vw;
height:100vh;
}
.wrapper-list {
min-width: 330px;
border: 1px solid var(--color-beta-on-surface_border);
border-left: 0px;
overflow: scroll;
}
.wrapper-content {
margin-left: 15vw;
padding:50px;
height:100vh;
}
.selected {
border-left: 8px solid var(--color-beta-on-surface_border);
background-color: var(--color-surface-accent);
padding-right: Opx;
}
.list--item {
border-bottom: 1px solid #D9DDE1;
padding: 14px 30px;
}
.list--item__values h3 {
font-size: 14px;
font-weight: bold;
padding: 2px 0;
}
.list--item__values h3 span {
font-size: 10px;
text-transform: uppercase;
}
.list--item__values p {
font-size: 10px;
padding: 2px 0;
}
.list--item__values p span {
text-transform: uppercase;
}