瀏覽代碼

Swipe card feature + Partial UI

master
kj1352 4 年之前
父節點
當前提交
953af3b23f
共有 11 個文件被更改,包括 224 次插入22 次删除
  1. +5
    -0
      package-lock.json
  2. +1
    -0
      package.json
  3. +9
    -3
      src/app/app.module.ts
  4. +7
    -0
      src/app/hammer-config.spec.ts
  5. +7
    -0
      src/app/hammer-config.ts
  6. +1
    -1
      src/app/quiz/quiz.page.html
  7. +18
    -6
      src/app/quiz/quiz.page.ts
  8. +11
    -9
      src/app/quiz/swipe-cards/swipe-cards.component.html
  9. +41
    -1
      src/app/quiz/swipe-cards/swipe-cards.component.scss
  10. +123
    -2
      src/app/quiz/swipe-cards/swipe-cards.component.ts
  11. +1
    -0
      src/main.ts

+ 5
- 0
package-lock.json 查看文件

@@ -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",


+ 1
- 0
package.json 查看文件

@@ -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",


+ 9
- 3
src/app/app.module.ts 查看文件

@@ -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]
})


+ 7
- 0
src/app/hammer-config.spec.ts 查看文件

@@ -0,0 +1,7 @@
import { HammerConfig } from './hammer-config';

describe('HammerConfig', () => {
it('should create an instance', () => {
expect(new HammerConfig()).toBeTruthy();
});
});

+ 7
- 0
src/app/hammer-config.ts 查看文件

@@ -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}
}
}

+ 1
- 1
src/app/quiz/quiz.page.html 查看文件

@@ -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>


+ 18
- 6
src/app/quiz/quiz.page.ts 查看文件

@@ -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;
}
}



+ 11
- 9
src/app/quiz/swipe-cards/swipe-cards.component.html 查看文件

@@ -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>

+ 41
- 1
src/app/quiz/swipe-cards/swipe-cards.component.scss 查看文件

@@ -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;
}
}
}

+ 123
- 2
src/app/quiz/swipe-cards/swipe-cards.component.ts 查看文件

@@ -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
- 0
src/main.ts 查看文件

@@ -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';


Loading…
取消
儲存