Skip to content

Commit

Permalink
feat: drag and drop voting (wip with test data)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwestfall committed Jan 1, 2024
1 parent 8972279 commit 6a61434
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/Client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/Client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.2",
"lodash-es": "^4.17.21",
"mobile-drag-drop": "^3.0.0-rc.0",
"moment": "^2.30.1",
"ngx-drag-drop": "^17.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ <h4 class="display-4 text-center">Recommendations</h4>
class="d-flex flex-column gap-2 align-items-center">
<h4 class="display-4">Voting is OPEN!</h4>
<!-- todo: rank recommendations -->
<div>
<app-live-voting
*ngIf="(meeting$ | async)?.bookRecommendations"
[recommendations]="(meeting$ | async)?.bookRecommendations!">
</app-live-voting>
</div>
<button
class="btn btn-primary"
*ngIf="admin$ | async"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.dndList {
transition: all 1ms ease;
padding: 5px;
}

.dndDragging {
border: 1px solid green;
}

.dndDraggingSource {
display: none;
}

.dndPlaceholder {
height: 4rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="d-flex flex-row gap-3">
<div>
<ul
class="list-group"
dndEffectAllowed="move"
(dndDrop)="onDrop($event, rankedRecommendations)"
dndDropzone>
<li
class="list-group-item list-group-item-info dndPlaceholder"
dndPlaceholderRef></li>
<li
*ngFor="let rec of rankedRecommendations"
class="list-group-item"
[ngClass]="{ 'list-group-item-success': rec.isMine }"
[dndDraggable]="rec"
(dndMoved)="onDragged(rec, rankedRecommendations)"
dndEffectAllowed="move">
{{ rec.rank }}: {{ rec.book.title }} - {{ rec.recommendedBy.firstName }}
</li>
</ul>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { LiveVotingComponent } from './live-voting.component';

describe('LiveVotingComponent', () => {
let component: LiveVotingComponent;
let fixture: ComponentFixture<LiveVotingComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LiveVotingComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(LiveVotingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { DndDropEvent } from 'ngx-drag-drop';
import { Subscription } from 'rxjs';
import {
BookDto,
BookRecommendationForMeetingDto,
UserSimpleDto,
} from '../../../api/models';
import { AppState } from '../../../app-state';
import { selectAuthenticatedUser } from '../../../users/state/users.selectors';

export const testData: RankedBook[] = [
{
book: {
title: 'The Hobbit',
author: 'J.R.R. Tolkien',
id: '1',
},
recommendedBy: {
firstName: 'Luke',
lastName: 'Doe',
},
rank: 1,
isMine: true,
},
{
book: {
title: 'The Fellowship of the Ring',
author: 'J.R.R. Tolkien',
id: '2',
},
recommendedBy: {
firstName: 'Jane',
lastName: 'Doe',
},
rank: 2,
isMine: false,
},
{
book: {
title: 'The Two Towers',
author: 'J.R.R. Tolkien',
id: '3',
},
recommendedBy: {
firstName: 'Mike',
lastName: 'Smith',
},
rank: 3,
isMine: false,
},
{
book: {
title: 'The Return of the King',
author: 'J.R.R. Tolkien',
id: '4',
},
recommendedBy: {
firstName: 'Bill',
lastName: 'Jones',
},
rank: 4,
isMine: false,
},
];

@Component({
selector: 'app-live-voting',
templateUrl: './live-voting.component.html',
styleUrls: ['./live-voting.component.css'],
})
export class LiveVotingComponent implements OnInit, OnDestroy {
@Input({ required: true })
recommendations!: BookRecommendationForMeetingDto[];

rankedRecommendations: RankedBook[] = [];

userSubscription?: Subscription;

constructor(private store: Store<AppState>) {}

ngOnInit() {
const userObs$ = this.store.select(selectAuthenticatedUser);

this.userSubscription = userObs$.subscribe(user => {
if (!user) {
return;
}

this.rankedRecommendations = testData;

// todo: reenable this
// const myRec = this.recommendations.find(
// // todo: improve this check
// r =>
// r.recommendedBy.firstName === user.firstName &&
// r.recommendedBy.lastName === user.lastName
// );

// if (myRec) {
// this.rankedRecommendations.push({
// book: myRec?.book,
// recommendedBy: myRec?.recommendedBy,
// rank: 1,
// isMine: true,
// });
// }

// this.recommendations
// .filter(r => r !== myRec)
// .forEach(r => {
// this.rankedRecommendations.push({
// book: r.book,
// recommendedBy: r.recommendedBy,
// rank: this.rankedRecommendations.length + 1,
// isMine: false,
// });
// });
});
}

ngOnDestroy() {
this.userSubscription?.unsubscribe();
}

onDrop(event: DndDropEvent, list?: RankedBook[]) {
if (list && (event.dropEffect === 'copy' || event.dropEffect === 'move')) {
let index = event.index;

if (typeof index === 'undefined') {
index = list.length;
}

list.splice(index, 0, event.data);
}
}

onDragged(item: RankedBook, list: RankedBook[]) {
// this happens after onDrop

const index = list.indexOf(item);
list.splice(index, 1);
list.forEach((r, i) => {
r.rank = i + 1;
});

// todo: update votes via signalr
}
}

interface RankedBook {
book: BookDto;
recommendedBy: UserSimpleDto;
rank: number;
isMine: boolean;
}
4 changes: 4 additions & 0 deletions src/Client/src/app/meetings/meetings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { DndModule } from 'ngx-drag-drop';
import { BooksModule } from '../books/books.module';
import { LiveMeetingComponent } from './live-meeting/live-meeting.component';
import { LiveVotingComponent } from './live-meeting/live-voting/live-voting.component';
import { MeetingCountdownComponent } from './meeting-countdown/meeting-countdown.component';
import { MeetingsPageComponent } from './meetings-page/meetings-page.component';
import { NextMeetingCardComponent } from './next-meeting-card/next-meeting-card.component';
Expand All @@ -18,12 +20,14 @@ import { meetingsReducer } from './state/meetings.reducer';
StoreModule.forFeature('meetings', meetingsReducer),
EffectsModule.forFeature([MeetingsEffects]),
BooksModule,
DndModule,
],
declarations: [
MeetingCountdownComponent,
MeetingsPageComponent,
NextMeetingCardComponent,
LiveMeetingComponent,
LiveVotingComponent,
],
exports: [MeetingCountdownComponent, NextMeetingCardComponent],
})
Expand Down
15 changes: 15 additions & 0 deletions src/Client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
/// <reference types="@angular/localize" />

import { bootstrapApplication } from '@angular/platform-browser';
import { polyfill } from 'mobile-drag-drop';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
// optional import of scroll behaviour
import { scrollBehaviourDragImageTranslateOverride } from 'mobile-drag-drop/scroll-behaviour';

polyfill({
// use this to make use of the scroll behaviour
dragImageTranslateOverride: scrollBehaviourDragImageTranslateOverride,
});

// workaround to make scroll prevent work in iOS Safari >= 10
try {
window.addEventListener('touchmove', function () {}, { passive: false });
} catch (e) {
/* empty */
}

bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));

0 comments on commit 6a61434

Please sign in to comment.