@@ -6134,6 +6134,11 @@ | |||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", | |||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" | |||
}, | |||
"hammerjs": { | |||
"version": "2.0.8", | |||
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", | |||
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" | |||
}, | |||
"handle-thing": { | |||
"version": "2.0.1", | |||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | |||
@@ -26,6 +26,7 @@ | |||
"@ionic/angular": "^5.0.0", | |||
"cordova-res": "^0.15.2", | |||
"faker": "^5.1.0", | |||
"hammerjs": "^2.0.8", | |||
"moment": "^2.29.1", | |||
"rxjs": "~6.5.5", | |||
"sharp": "^0.27.0", | |||
@@ -1,5 +1,5 @@ | |||
import { NgModule } from '@angular/core'; | |||
import { BrowserModule } from '@angular/platform-browser'; | |||
import { BrowserModule, HAMMER_GESTURE_CONFIG, HammerModule } from '@angular/platform-browser'; | |||
import { RouteReuseStrategy } from '@angular/router'; | |||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; | |||
@@ -10,15 +10,21 @@ import { AppRoutingModule } from './app-routing.module'; | |||
import { AppComponent } from './app.component'; | |||
import { ServiceWorkerModule } from '@angular/service-worker'; | |||
import { environment } from '../environments/environment'; | |||
import { MyHammerConfig } from './hammer-config'; | |||
@NgModule({ | |||
declarations: [AppComponent], | |||
entryComponents: [], | |||
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })], | |||
imports: [BrowserModule, | |||
IonicModule.forRoot(), | |||
AppRoutingModule, | |||
HammerModule, | |||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })], | |||
providers: [ | |||
StatusBar, | |||
SplashScreen, | |||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy } | |||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, | |||
{ provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, | |||
], | |||
bootstrap: [AppComponent] | |||
}) | |||
@@ -0,0 +1,7 @@ | |||
import { HammerConfig } from './hammer-config'; | |||
describe('HammerConfig', () => { | |||
it('should create an instance', () => { | |||
expect(new HammerConfig()).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,7 @@ | |||
import * as Hammer from 'hammerjs'; | |||
import {HammerGestureConfig} from '@angular/platform-browser'; | |||
export class MyHammerConfig extends HammerGestureConfig { | |||
overrides = { | |||
'swipe': {direction: Hammer.DIRECTION_ALL} | |||
} | |||
} |
@@ -33,7 +33,7 @@ | |||
</div> | |||
<button class="next-button" (click)="nextQuestion()"> | |||
<button class="next-button" *ngIf="questionType === 'MCQ'" (click)="nextQuestion()"> | |||
<span class="text"> Next </span> | |||
<span class="dot"></span> | |||
@@ -8,11 +8,13 @@ import * as faker from 'faker'; | |||
}) | |||
export class QuizPage implements OnInit { | |||
secondsPerQuestion: number; | |||
selectedQuestion: number = 1; | |||
selectedQuestion: number = 0; | |||
questionType: 'MCQ' | 'CARD'; | |||
questions: Array<{ | |||
type: 'MCQ' | 'CARD', | |||
question: string | Array<string>, | |||
question: any, | |||
choices?: Array<{ | |||
value: string, | |||
text: string, | |||
@@ -31,7 +33,16 @@ export class QuizPage implements OnInit { | |||
} else { | |||
this.secondsPerQuestion -= 1; | |||
} | |||
}, 1000); | |||
}, 1000); | |||
let cardQuestions = []; | |||
for (let i = 0; i < 4; i += 1) { | |||
cardQuestions.push({ | |||
text: faker.lorem.sentence(), | |||
image: faker.image.abstract() | |||
}); | |||
} | |||
this.questions = [{ | |||
type: 'MCQ', | |||
@@ -50,12 +61,12 @@ export class QuizPage implements OnInit { | |||
secondsAllotted: 50 | |||
}, { | |||
type: 'CARD', | |||
question: [faker.lorem.sentence(), faker.lorem.sentence(), faker.lorem.sentence(), faker.lorem.sentence()], | |||
secondsAllotted: 50 | |||
question: cardQuestions, | |||
secondsAllotted: 20 | |||
}]; | |||
this.secondsPerQuestion = this.questions[this.selectedQuestion].secondsAllotted; | |||
this.questionType = this.questions[this.selectedQuestion].type; | |||
} | |||
@@ -63,6 +74,7 @@ export class QuizPage implements OnInit { | |||
if (this.selectedQuestion < this.questions.length - 1) { | |||
this.selectedQuestion += 1; | |||
this.secondsPerQuestion = this.questions[this.selectedQuestion].secondsAllotted; | |||
this.questionType = this.questions[this.selectedQuestion].type; | |||
} | |||
} | |||
@@ -1,13 +1,15 @@ | |||
<div class="container"> | |||
<ion-card #tinderCard *ngFor="let question of questions; let i = index" | |||
[ngStyle]="{ zIndex: questions.length - i, transform: 'scale(' + (20 - i) / 20 + ') translateY(-' + 20 * i + 'px)' }"> | |||
<section class="card" #tinderCard *ngFor="let question of questions; let i = index" | |||
[ngStyle]="{ zIndex: questions.length - i, transform: 'scale(' + (20 - i) / 20 + ') translateY(-' + 20 * i + 'px)' }" | |||
(pan)="handlePan($event)" (panend)="handlePanEnd($event)"> | |||
<p> | |||
{{ question }} | |||
{{ question.text }} | |||
</p> | |||
<div class="action-buttons"> | |||
<button> True </button> | |||
<button> False </button> | |||
</div> | |||
</ion-card> | |||
</section> | |||
<div class="action-buttons"> | |||
<button (click)="userClickedButton($event, false)"> <ion-icon name="close-outline"></ion-icon> </button> | |||
<button (click)="userClickedButton($event, True)"> <ion-icon name="checkmark"></ion-icon> </button> | |||
</div> | |||
</div> |
@@ -10,7 +10,7 @@ | |||
} | |||
ion-card { | |||
.card { | |||
margin: 0; | |||
padding: 15px; | |||
@@ -27,4 +27,44 @@ ion-card { | |||
cursor: -webkit-grab; | |||
cursor: -moz-grab; | |||
cursor: grab; | |||
} | |||
.action-buttons { | |||
display: flex; | |||
position: fixed; | |||
z-index: 1; | |||
bottom: 10vh; | |||
width: 120px; | |||
left: calc(50% - 60px); | |||
overflow: visible; | |||
align-items: center; | |||
justify-content: space-between; | |||
height: 50px; | |||
background-color: white; | |||
box-shadow: 0px 0px 10px 0px $dark-blue; | |||
button { | |||
width: 70px; | |||
height: 70px; | |||
background-color: white; | |||
border-radius: 50%; | |||
border: 0px; | |||
font-size: 1.7rem; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
&:first-child { | |||
margin-left: -30px; | |||
color: $pink; | |||
box-shadow: -5px 0px 10px 0px $dark-blue; | |||
} | |||
&:last-child { | |||
margin-right: -30px; | |||
color: $green; | |||
box-shadow: 5px 0px 10px 0px $dark-blue; | |||
} | |||
} | |||
} |
@@ -1,4 +1,4 @@ | |||
import { Component, OnInit, Input, ViewChildren, QueryList, ElementRef } from '@angular/core'; | |||
import { Component, OnInit, Input, ViewChildren, QueryList, ElementRef, Renderer2, Output, EventEmitter } from '@angular/core'; | |||
@Component({ | |||
selector: 'app-swipe-cards', | |||
@@ -9,8 +9,129 @@ export class SwipeCardsComponent implements OnInit { | |||
@ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>; | |||
@Input() questions: Array<string>; | |||
constructor() { } | |||
moveOutWidth: number; // value in pixels that a card needs to travel to dissapear from screen | |||
shiftRequired: boolean; // state variable that indicates we need to remove the top card of the stack | |||
transitionInProgress: boolean; // state variable that indicates currently there is transition on-going | |||
heartVisible: boolean; | |||
crossVisible: boolean; | |||
tinderCardsArray: Array<ElementRef>; | |||
@Output() cardEvent = new EventEmitter(); | |||
constructor( | |||
private renderer: Renderer2 | |||
) { } | |||
ngOnInit() {} | |||
ngAfterViewInit() { | |||
this.moveOutWidth = document.documentElement.clientWidth * 1.5; | |||
this.tinderCardsArray = this.tinderCards.toArray(); | |||
this.tinderCards.changes.subscribe(()=>{ | |||
this.tinderCardsArray = this.tinderCards.toArray(); | |||
}); | |||
} | |||
toggleChoiceIndicator(cross, heart) { | |||
this.crossVisible = cross; | |||
this.heartVisible = heart; | |||
} | |||
handleShift() { | |||
this.transitionInProgress = false; | |||
this.toggleChoiceIndicator(false,false) | |||
if (this.shiftRequired) { | |||
this.shiftRequired = false; | |||
this.questions.shift(); | |||
}; | |||
} | |||
handlePan(event) { | |||
if (event.deltaX === 0 || (event.center.x === 0 && event.center.y === 0) || !this.questions.length) return; | |||
if (this.transitionInProgress) { | |||
this.handleShift(); | |||
} | |||
this.renderer.addClass(this.tinderCardsArray[0].nativeElement, 'moving'); | |||
if (event.deltaX > 0) { this.toggleChoiceIndicator(false,true) } | |||
if (event.deltaX < 0) { this.toggleChoiceIndicator(true,false) } | |||
let xMulti = event.deltaX * 0.03; | |||
let yMulti = event.deltaY / 80; | |||
let rotate = xMulti * yMulti; | |||
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + event.deltaX + 'px, ' + event.deltaY + 'px) rotate(' + rotate + 'deg)'); | |||
this.shiftRequired = true; | |||
} | |||
handlePanEnd(event) { | |||
this.toggleChoiceIndicator(false,false); | |||
if (!this.questions.length) return; | |||
this.renderer.removeClass(this.tinderCardsArray[0].nativeElement, 'moving'); | |||
let keep = Math.abs(event.deltaX) < 80 || Math.abs(event.velocityX) < 0.5; | |||
if (keep) { | |||
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', ''); | |||
this.shiftRequired = false; | |||
} else { | |||
let endX = Math.max(Math.abs(event.velocityX) * this.moveOutWidth, this.moveOutWidth); | |||
let toX = event.deltaX > 0 ? endX : -endX; | |||
let endY = Math.abs(event.velocityY) * this.moveOutWidth; | |||
let toY = event.deltaY > 0 ? endY : -endY; | |||
let xMulti = event.deltaX * 0.03; | |||
let yMulti = event.deltaY / 80; | |||
let rotate = xMulti * yMulti; | |||
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + toX + 'px, ' + (toY + event.deltaY) + 'px) rotate(' + rotate + 'deg)'); | |||
this.shiftRequired = true; | |||
console.log(!!(event.deltaX > 0), this.questions[0]); | |||
setTimeout(() => { | |||
this.handleShift(); | |||
}, 350); | |||
} | |||
this.transitionInProgress = true; | |||
} | |||
userClickedButton(event, heart) { | |||
event.preventDefault(); | |||
if (!this.questions.length) return false; | |||
if (heart) { | |||
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(' + this.moveOutWidth + 'px, -100px) rotate(-30deg)'; | |||
this.toggleChoiceIndicator(false,true); | |||
console.log(!!(event.deltaX > 0), this.questions[0]); | |||
setTimeout(() => { | |||
this.handleShift(); | |||
}, 350); | |||
} else { | |||
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(-' + this.moveOutWidth + 'px, -100px) rotate(30deg)'; | |||
this.toggleChoiceIndicator(true,false); | |||
console.log(!!(event.deltaX > 0), this.questions[0]); | |||
setTimeout(() => { | |||
this.handleShift(); | |||
}, 350); | |||
}; | |||
this.shiftRequired = true; | |||
this.transitionInProgress = true; | |||
} | |||
} |
@@ -1,5 +1,6 @@ | |||
import { enableProdMode } from '@angular/core'; | |||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |||
import 'hammerjs'; | |||
import { AppModule } from './app/app.module'; | |||
import { environment } from './environments/environment'; | |||