@@ -6134,6 +6134,11 @@ | |||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", | ||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" | "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": { | "handle-thing": { | ||||
"version": "2.0.1", | "version": "2.0.1", | ||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | ||||
@@ -26,6 +26,7 @@ | |||||
"@ionic/angular": "^5.0.0", | "@ionic/angular": "^5.0.0", | ||||
"cordova-res": "^0.15.2", | "cordova-res": "^0.15.2", | ||||
"faker": "^5.1.0", | "faker": "^5.1.0", | ||||
"hammerjs": "^2.0.8", | |||||
"moment": "^2.29.1", | "moment": "^2.29.1", | ||||
"rxjs": "~6.5.5", | "rxjs": "~6.5.5", | ||||
"sharp": "^0.27.0", | "sharp": "^0.27.0", | ||||
@@ -1,5 +1,5 @@ | |||||
import { NgModule } from '@angular/core'; | 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 { RouteReuseStrategy } from '@angular/router'; | ||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; | ||||
@@ -10,15 +10,21 @@ import { AppRoutingModule } from './app-routing.module'; | |||||
import { AppComponent } from './app.component'; | import { AppComponent } from './app.component'; | ||||
import { ServiceWorkerModule } from '@angular/service-worker'; | import { ServiceWorkerModule } from '@angular/service-worker'; | ||||
import { environment } from '../environments/environment'; | import { environment } from '../environments/environment'; | ||||
import { MyHammerConfig } from './hammer-config'; | |||||
@NgModule({ | @NgModule({ | ||||
declarations: [AppComponent], | declarations: [AppComponent], | ||||
entryComponents: [], | 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: [ | providers: [ | ||||
StatusBar, | StatusBar, | ||||
SplashScreen, | SplashScreen, | ||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy } | |||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, | |||||
{ provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, | |||||
], | ], | ||||
bootstrap: [AppComponent] | 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> | </div> | ||||
<button class="next-button" (click)="nextQuestion()"> | |||||
<button class="next-button" *ngIf="questionType === 'MCQ'" (click)="nextQuestion()"> | |||||
<span class="text"> Next </span> | <span class="text"> Next </span> | ||||
<span class="dot"></span> | <span class="dot"></span> | ||||
@@ -8,11 +8,13 @@ import * as faker from 'faker'; | |||||
}) | }) | ||||
export class QuizPage implements OnInit { | export class QuizPage implements OnInit { | ||||
secondsPerQuestion: number; | secondsPerQuestion: number; | ||||
selectedQuestion: number = 1; | |||||
selectedQuestion: number = 0; | |||||
questionType: 'MCQ' | 'CARD'; | |||||
questions: Array<{ | questions: Array<{ | ||||
type: 'MCQ' | 'CARD', | type: 'MCQ' | 'CARD', | ||||
question: string | Array<string>, | |||||
question: any, | |||||
choices?: Array<{ | choices?: Array<{ | ||||
value: string, | value: string, | ||||
text: string, | text: string, | ||||
@@ -31,7 +33,16 @@ export class QuizPage implements OnInit { | |||||
} else { | } else { | ||||
this.secondsPerQuestion -= 1; | 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 = [{ | this.questions = [{ | ||||
type: 'MCQ', | type: 'MCQ', | ||||
@@ -50,12 +61,12 @@ export class QuizPage implements OnInit { | |||||
secondsAllotted: 50 | secondsAllotted: 50 | ||||
}, { | }, { | ||||
type: 'CARD', | 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.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) { | if (this.selectedQuestion < this.questions.length - 1) { | ||||
this.selectedQuestion += 1; | this.selectedQuestion += 1; | ||||
this.secondsPerQuestion = this.questions[this.selectedQuestion].secondsAllotted; | this.secondsPerQuestion = this.questions[this.selectedQuestion].secondsAllotted; | ||||
this.questionType = this.questions[this.selectedQuestion].type; | |||||
} | } | ||||
} | } | ||||
@@ -1,13 +1,15 @@ | |||||
<div class="container"> | <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> | <p> | ||||
{{ question }} | |||||
{{ question.text }} | |||||
</p> | </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> | </div> |
@@ -10,7 +10,7 @@ | |||||
} | } | ||||
ion-card { | |||||
.card { | |||||
margin: 0; | margin: 0; | ||||
padding: 15px; | padding: 15px; | ||||
@@ -27,4 +27,44 @@ ion-card { | |||||
cursor: -webkit-grab; | cursor: -webkit-grab; | ||||
cursor: -moz-grab; | cursor: -moz-grab; | ||||
cursor: 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({ | @Component({ | ||||
selector: 'app-swipe-cards', | selector: 'app-swipe-cards', | ||||
@@ -9,8 +9,129 @@ export class SwipeCardsComponent implements OnInit { | |||||
@ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>; | @ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>; | ||||
@Input() questions: Array<string>; | @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() {} | 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 { enableProdMode } from '@angular/core'; | ||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
import 'hammerjs'; | |||||
import { AppModule } from './app/app.module'; | import { AppModule } from './app/app.module'; | ||||
import { environment } from './environments/environment'; | import { environment } from './environments/environment'; | ||||