...
 
Commits (151)
# ARSnova lite
# frag.jetzt
This is the new frontend for the Audience Response System [ARSnova](https://arsnova.eu) and is currently in beta.
The project aims at creating the best possible user experience with a web-based audience response app. You can head over to [ARSnova lite](https://beta.arsnova.eu) to see it in action.
Nomen est omen: The app's name says it all: it stands for both the app's main purpose and the web address https://frag.jetzt
## Documentation
......@@ -10,4 +8,4 @@ The project aims at creating the best possible user experience with a web-based
## Credits
frag.jetzt is powered by Technische Hochschule Mittelhessen - University of Applied Sciences.
frag.jetzt is powered by Technische Hochschule Mittelhessen | University of Applied Sciences.
......@@ -138,4 +138,4 @@
"prefix": "app"
}
}
}
\ No newline at end of file
}
frag.jetzt @ d65b08bf
Subproject commit d65b08bf7e4298c562502be84e114e6afbb5d2cc
# Ars
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.0.
## Code scaffolding
Run `ng generate component component-name --project ars` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ars`.
> Note: Don't forget to add `--project ars` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build ars` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build ars`, go to the dist folder `cd dist/ars` and run `npm publish`.
## Running unit tests
Run `ng test ars` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/ars'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ars",
"lib": {
"entryFile": "src/public-api.ts"
}
}
\ No newline at end of file
{
"name": "ars",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^7.2.0",
"@angular/core": "^7.2.0"
}
}
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArsComponent } from './ars.component';
describe('ArsComponent', () => {
let component: ArsComponent;
let fixture: ComponentFixture<ArsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ArsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
template: `
<p>
ars works!
</p>
`,
styles: []
})
// tslint:disable-next-line:directive-class-suffix
export class ArsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
import { NgModule } from '@angular/core';
import { ArsComponent } from './ars.component';
import { ArsSliderDirective } from './components/io/slider/ars-slider.directive';
import { ArsSliderCombComponent } from './components/io/slider/ars-slider-comb/ars-slider-comb.component';
import { MatButtonModule, MatIconModule } from '@angular/material';
@NgModule({
declarations: [
ArsComponent,
ArsSliderDirective,
ArsSliderCombComponent
],
imports: [
MatIconModule,
MatButtonModule
],
exports: [
ArsComponent,
ArsSliderDirective,
ArsSliderCombComponent
]
})
export class ArsModule { }
import { TestBed } from '@angular/core/testing';
import { ArsService } from './ars.service';
describe('ArsService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ArsService = TestBed.get(ArsService);
expect(service).toBeTruthy();
});
});
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ArsService {
constructor() { }
}
<ng-container>
<mat-icon (click)="slider.prev();">{{leftIcon}}</mat-icon>
<ng-content>
</ng-content>
<mat-icon (click)="slider.next();">{{rightIcon}}</mat-icon>
</ng-container>
:host{
width:100%;
height:48px;
display:flex;
justify-content:space-between;
flex-direction:row;
}
mat-icon{
padding:12px 5px;
margin:0;
display:flex;
float:left;
flex-shrink:0;
cursor:pointer;
-webkit-touch-callout:none;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArsSliderCombComponent } from './ars-slider-comb.component';
describe('ArsSliderCombComponent', () => {
let component: ArsSliderCombComponent;
let fixture: ComponentFixture<ArsSliderCombComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ArsSliderCombComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArsSliderCombComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { AfterViewInit, Component, ContentChild, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import { ArsSliderDirective } from '../ars-slider.directive';
@Component({
selector: 'ars-slider-comb',
templateUrl: './ars-slider-comb.component.html',
styleUrls: ['./ars-slider-comb.component.scss']
})
export class ArsSliderCombComponent implements OnInit, AfterViewInit {
@Input() width: number;
@Input() leftIcon = 'keyboard_arrow_left';
@Input() rightIcon = 'keyboard_arrow_right';
@ContentChild(ArsSliderDirective) slider: ArsSliderDirective;
constructor(private ref: ElementRef, private render: Renderer2) {
}
ngOnInit() {
}
ngAfterViewInit() {
this.slider.setWidth(this.width - 68);
if (this.width) {
this.render.setStyle(this.ref.nativeElement, 'width', this.width + 'px');
}
}
}
import { AfterViewInit, Directive, Input, OnInit, Renderer2 } from '@angular/core';
import { MatSlider } from '@angular/material';
@Directive({
selector: '[ars-slider]'
})
export class ArsSliderDirective implements OnInit, AfterViewInit {
public static classes: Object = {
'mat-slider-wrapper': '',
'mat-slider-track-wrapper': 'rgba(127,127,127,0.5)',
'mat-slider-track-background': '',
'mat-slider-track-fill': 'var(--on-surface)',
'mat-slider-ticks-container': '',
'mat-slider-ticks': '',
'mat-slider-focus-ring': 'var(--on-surface)',
'mat-slider-thumb': 'var(--on-surface)',
'mat-slider-thumb-container': 'var(--on-surface)',
'mat-slider-thumb-label': '',
'mat-slider-thumb-label-text': ''
};
@Input() width: number;
private elem: HTMLInputElement;
constructor(private slider: MatSlider, private render: Renderer2) {
this.elem = slider._elementRef.nativeElement;
}
ngOnInit() {
}
ngAfterViewInit() {
Array.from(this.elem.getElementsByTagName('*')).forEach(e => {
if (ArsSliderDirective.classes.hasOwnProperty(e.className)) {
(<HTMLElement>e).style.background = ArsSliderDirective.classes[e.className];
}
});
this.render.setStyle(this.elem, 'height', '48px');
this.render.setStyle(this.elem, 'minHeight', '48px');
this.render.setStyle(this.elem, 'maxHeight', '48px');
this.updateWidth();
}
public setWidth(width: number) {
this.width = width;
this.updateWidth();
}
private updateWidth() {
this.render.setStyle(this.elem, 'width', this.width + 'px');
this.render.setStyle(this.elem, 'minWidth', this.width + 'px');
this.render.setStyle(this.elem, 'maxWidth', this.width + 'px');
}
public next() {
this.slider.value += this.slider.step;
if (this.slider.value > this.slider.max) {
this.slider.value = this.slider.max;
}
}
public prev() {
this.slider.value -= this.slider.step;
if (this.slider.value < this.slider.min) {
this.slider.value = this.slider.min;
}
}
}
/*
* Public API Surface of ars
*/
export * from './lib/ars.service';
export * from './lib/ars.component';
export * from './lib/ars.module';
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"module": "es2015",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}
......@@ -15,6 +15,14 @@
},
"logLevel": "debug"
},
"/api/bonustoken": {
"target": "http://localhost:8088",
"secure": false,
"pathRewrite": {
"^/api": ""
},
"logLevel": "debug"
},
"/api/settings": {
"target": "http://localhost:8088",
"secure": false,
......
<script src="models/rescale.ts"></script>
<div fxLayout="column" fxFill appTheme>
<div fxLayout="column" fxFill appTheme id="outer_main_container">
<header class="header" id="header_rescale">
<app-header #header></app-header>
</header>
<div id="rescale_screen">
<div class="main_container">
<div class="main_container" id="scroll_container">
<div class="inner_main_container">
<main fxFlex class="app-component">
<router-outlet></router-outlet>
......@@ -17,7 +17,7 @@
</footer>
<div class="rescale_overlay" id="overlay_rescale" aria-hidden="true">
<button mat-icon-button (click)="getRescale().toggleState()" title="toggle">
<mat-icon>{{getRescale().getState()==1?'fullscreen':'fullscreen_exit'}}</mat-icon>
<mat-icon>fullscreen_exit</mat-icon>
</button>
<button mat-icon-button (click)="getRescale().scaleUp()" title="Zoom +">
<mat-icon>zoom_in</mat-icon>
......@@ -30,3 +30,4 @@
</button>
</div>
</div>
......@@ -47,16 +47,16 @@ main {
}
.rescale_overlay{
width:190px;
width:180px;
height:50px;
border-radius:0px 25px 25px 0px;
border-radius:25px 25px 25px 25px;
background-color:var(--surface);
position:fixed;
left:-100px;
top:15px;
left:10px;
top:-100px;
z-index:2;
display:none;
box-shadow:0px 2px 4px rgba(0,0,0,0.5);
box-shadow:0px 2px 2px rgba(0,0,0,0.2);
transition:all 0.2s ease-in-out;
}
......@@ -69,5 +69,5 @@ main {
}
.rescale_overlay>button:first-child{
margin-left:15px;
margin-left:10px;
}
......@@ -2,9 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { SwUpdate } from '@angular/service-worker';
import { NotificationService } from './services/util/notification.service';
import { MatIconRegistry } from '@angular/material';
import { DomSanitizer } from '@angular/platform-browser';
import { Rescale } from './models/rescale';
import { CustomIconService } from './services/util/custom-icon.service';
@Component({
selector: 'app-root',
......@@ -13,30 +12,34 @@ import { Rescale } from './models/rescale';
})
export class AppComponent implements OnInit {
public static rescale: Rescale = new Rescale();
icons = [
'beamer',
'meeting_room'
];
private static scrollAnimation = true;
constructor(private translationService: TranslateService,
private update: SwUpdate,
public notification: NotificationService,
private matIconRegistry: MatIconRegistry,
private domSanitizer: DomSanitizer) {
private customIconService: CustomIconService) {
translationService.setDefaultLang(this.translationService.getBrowserLang());
sessionStorage.setItem('currentLang', this.translationService.getBrowserLang());
for (const icon of this.icons) {
this.matIconRegistry.addSvgIcon(
icon,
this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/icons/' + icon + '.svg')
);
}
customIconService.init();
}
public static rescale: Rescale = new Rescale();
title = 'frag.jetzt';
public static scrollTop() {
const sc: HTMLElement = document.getElementById('scroll_container');
if (AppComponent.scrollAnimation) {
sc.scrollTo({ top: 0, behavior: 'smooth' });
} else {
sc.scrollTop = 0;
}
}
public static isScrolledTop(): boolean {
return document.getElementById('scroll_container').scrollTop === 0;
}
ngOnInit(): void {
this.update.available.subscribe(update => {
let install: string;
......
......@@ -25,7 +25,7 @@ import { CreatorModule } from './components/creator/creator.module';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { LanguageService } from './services/util/language.service';
import { MarkdownService, MarkedOptions } from 'ngx-markdown';
import { MarkdownModule, MarkdownService, MarkedOptions } from 'ngx-markdown';
import { NewLandingComponent } from './components/home/new-landing/new-landing.component';
import { HomePageComponent } from './components/home/home-page/home-page.component';
import { UserHomeComponent } from './components/home/user-home/user-home.component';
......@@ -41,13 +41,15 @@ import { DemoVideoComponent } from './components/home/_dialogs/demo-video/demo-v
import { HomeCreatorPageComponent } from './components/home/home-creator-page/home-creator-page.component';
import { HomeParticipantPageComponent } from './components/home/home-participant-page/home-participant-page.component';
import { CommentSettingsService } from './services/http/comment-settings.service';
import { BonusTokenService } from './services/http/bonus-token.service';
import { CustomIconService } from './services/util/custom-icon.service';
import { ModeratorModule } from './components/moderator/moderator.module';
import { ImprintComponent } from './components/home/_dialogs/imprint/imprint.component';
import { DataProtectionComponent } from './components/home/_dialogs/data-protection/data-protection.component';
import { HelpPageComponent } from './components/shared/_dialogs/help-page/help-page.component';
import { CookiesComponent } from './components/home/_dialogs/cookies/cookies.component';
import { DataProtectionEnComponent } from '../assets/i18n/data-protection/data-protection-en';
import { DataProtectionDeComponent } from 'assets/i18n/data-protection/data-protection-de';
import { DataProtectionDeComponent } from '../assets/i18n/data-protection/data-protection-de';
import { CookiesEnComponent } from '../assets/i18n/cookies/cookies-en';
import { CookiesDeComponent } from '../assets/i18n/cookies/cookies-de';
import { ImprintEnComponent } from '../assets/i18n/imprint/imprint-en';
......@@ -57,6 +59,7 @@ import { HelpEnComponent } from '../assets/i18n/help/help-en';
import { OverlayComponent } from './components/home/_dialogs/overlay/overlay.component';
import { DemoDeComponent } from '../assets/i18n/demo/demo-de';
import { DemoEnComponent } from '../assets/i18n/demo/demo-en';
import { ArsModule } from '../../projects/ars/src/lib/ars.module';
export function dialogClose(dialogResult: any) {
}
......@@ -112,6 +115,12 @@ export function initializeApp(appConfig: AppConfig) {
ThemeModule,
CreatorModule,
ModeratorModule,
MarkdownModule.forRoot({
provide: MarkedOptions,
useValue: {
sanitize: true
}
}),
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
TranslateModule.forChild({
loader: {
......@@ -120,7 +129,8 @@ export function initializeApp(appConfig: AppConfig) {
deps: [HttpClient]
},
isolate: true
})
}),
ArsModule
],
providers: [
/*AppConfig,
......@@ -150,6 +160,8 @@ export function initializeApp(appConfig: AppConfig) {
VoteService,
ModeratorService,
CommentSettingsService,
BonusTokenService,
CustomIconService,
WsConnectorService,
{
provide: MatDialogRef,
......
<h2>{{ 'room-page.sure' | translate }}</h2>
<mat-divider></mat-divider>
<p>{{reallyDeleteText}}</p>
<app-dialog-action-buttons
buttonsLabelSection="content"
confirmButtonLabel="delete"
[confirmButtonType]=confirmButtonType
[cancelButtonClickAction]="buildCloseDialogActionCallback()"
[confirmButtonClickAction]="buildCommentsDeleteActionCallback()"
></app-dialog-action-buttons>
import { Component, Inject, OnInit } from '@angular/core';
import { DialogConfirmActionButtonType } from '../../../shared/dialog/dialog-action-buttons/dialog-action-buttons.component';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { TranslateService } from '@ngx-translate/core';
import { BonusTokenComponent } from '../bonus-token/bonus-token.component';
@Component({
selector: 'app-bonus-delete',
templateUrl: './bonus-delete.component.html',
styleUrls: ['./bonus-delete.component.scss']
})
export class BonusDeleteComponent implements OnInit {
confirmButtonType: DialogConfirmActionButtonType = DialogConfirmActionButtonType.Alert;
multipleBonuses: boolean;
reallyDeleteText: string;
constructor(public dialogRef: MatDialogRef<BonusTokenComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private liveAnnouncer: LiveAnnouncer,
private translationService: TranslateService ) { }
ngOnInit() {
if (!this.multipleBonuses) {
this.translationService.get('room-page.really-delete-token').subscribe(msg => {
this.reallyDeleteText = msg;
});
} else {
this.translationService.get('room-page.really-delete-tokens').subscribe(msg => {
this.reallyDeleteText = msg;
});
}
this.liveAnnouncer.announce(this.reallyDeleteText);
}
/**
* Returns a lambda which closes the dialog on call.
*/
buildCloseDialogActionCallback(): () => void {
return () => this.dialogRef.close('abort');
}
/**
* Returns a lambda which executes the dialog dedicated action on call.
*/
buildCommentsDeleteActionCallback(): () => void {
return () => this.dialogRef.close('delete');
}
}
<div mat-dialog-content xmlns="http://www.w3.org/1999/html">
<h2>{{'room-page.bonus-token-header' | translate }}</h2>
<mat-divider></mat-divider>
<div *ngIf="bonusTokens.length >= 1">
<div fxLayout="row" *ngFor="let bonusToken of bonusTokens; index as i">
<p tabindex="0">
{{bonusToken.token}}
</p>
<span class="fill-remaining-space"></span>
<button mat-icon-button (click)="openDeleteSingleBonusDialog(bonusToken.userId, bonusToken.commentId, i)">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div *ngIf="bonusTokens.length === 0">
<p>{{'room-page.no-bonus' | translate }}<p>
</div>
<div fxLayoutAlign="center center" *ngIf="bonusTokens.length > 1">
<button mat-button class="delete" (click)="openDeleteAllBonusDialog()">
<mat-icon>delete</mat-icon>
{{'room-page.delete-all-tokens' | translate}}
</button>
</div>
<app-dialog-action-buttons
buttonsLabelSection="content"
[cancelButtonClickAction]="buildDeclineActionCallback()">
</app-dialog-action-buttons>
</div>
p {
color: var(--on-surface);
margin: 10px 0 10px 0;
}
.mat-icon-button {
margin-top: 5px;
color: var(--red);
}
.delete {
margin: 20px 0 20px 0;
min-width: 220px;
background-color: var(--red);
color: var(--on-secondary);
}
import { Component, OnInit } from '@angular/core';
import { BonusTokenService } from '../../../../services/http/bonus-token.service';
import { BonusToken } from '../../../../models/bonus-token';
import { MatDialog, MatDialogRef } from '@angular/material';
import { RoomCreatorPageComponent } from '../../room-creator-page/room-creator-page.component';
import { BonusDeleteComponent } from '../bonus-delete/bonus-delete.component';
import { NotificationService } from '../../../../services/util/notification.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-bonus-token',
templateUrl: './bonus-token.component.html',
styleUrls: ['./bonus-token.component.scss']
})
export class BonusTokenComponent implements OnInit {
roomId: string;
bonusTokens: BonusToken[] = [];
constructor(private bonusTokenService: BonusTokenService,
public dialog: MatDialog,
private dialogRef: MatDialogRef<RoomCreatorPageComponent>,
private translationService: TranslateService,
private notificationService: NotificationService) {
}
ngOnInit() {
this.bonusTokenService.getTokensByRoomId(this.roomId).subscribe( list => {
this.bonusTokens = list;
});
}
openDeleteSingleBonusDialog(userId: string, commentId: string, index: number): void {
const dialogRef = this.dialog.open(BonusDeleteComponent, {
width: '400px'
});
dialogRef.componentInstance.multipleBonuses = false;
dialogRef.afterClosed()
.subscribe(result => {
if (result === 'delete') {
this.deleteBonus(userId, commentId, index);
}
});
}
openDeleteAllBonusDialog(): void {
const dialogRef = this.dialog.open(BonusDeleteComponent, {
width: '400px'
});
dialogRef.componentInstance.multipleBonuses = true;
dialogRef.afterClosed()
.subscribe(result => {
if (result === 'delete') {
this.deleteAllBonuses();
}
});
}
deleteBonus(userId: string, commentId: string, index: number): void {
// Delete bonus via bonus-token-service
const toDelete = this.bonusTokens[index];
this.bonusTokenService.deleteToken(toDelete.roomId, toDelete.commentId, toDelete.userId).subscribe(_ => {
this.translationService.get('room-page.token-deleted').subscribe(msg => {
this.bonusTokens.splice(index, 1);
this.notificationService.show(msg);
});
});
}
deleteAllBonuses(): void {
// Delete all bonuses via bonus-token-service with roomId
this.bonusTokenService.deleteTokensByRoomId(this.roomId).subscribe(_ => {
this.translationService.get('room-page.tokens-deleted').subscribe(msg => {
this.dialogRef.close();
this.notificationService.show(msg);
});
});
}
/**
* Returns a lambda which closes the dialog on call.
*/
buildDeclineActionCallback(): () => void {
return () => this.dialogRef.close();
}
}
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="35px">
<h1>{{'comment-page.delimiter' | translate}}</h1>
<h2>{{'comment-page.delimiter' | translate}}</h2>
<mat-radio-group [(ngModel)]="exportType">
<div fxLayout="column">
<mat-radio-button value="comma" checked><p>{{'comment-page.comma' | translate}}</p></mat-radio-button>
......
......@@ -2,14 +2,7 @@ button {
min-width: 100px;
}
h1 {
font-size: large;
color: var(--on-surface);
}
p {
color: var(--on-surface);
}
.abort {
background-color: var(--secondary);
......
<div mat-dialog-content>
<h1>{{'room-page.comments' | translate }}</h1>
<h2>{{'room-page.comments' | translate }}</h2>
<mat-divider></mat-divider>
<div fxLayout="column">
<div fxLayout="row">
<h2>{{ 'room-page.threshold' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="settingThreshold" aria-labelledby="threshold" ></mat-slide-toggle></h2>
<p>{{ 'room-page.threshold' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="settingThreshold" aria-labelledby="threshold" ></mat-slide-toggle></p>
</div>
<div fxLayout="row" *ngIf="settingThreshold">
<mat-slider id="commentSlider" min="-100" max="0" step="5"
......@@ -14,12 +14,12 @@
</div>
<div fxLayout="column">
<div fxLayout="row">
<h2>{{ 'room-page.settings-comment-moderation' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="enableCommentModeration" aria-labelledby="settings-comment-moderation"></mat-slide-toggle></h2>
<p>{{ 'room-page.settings-comment-moderation' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="enableCommentModeration" aria-labelledby="settings-comment-moderation"></mat-slide-toggle></p>
</div>
<div fxLayout="row" *ngIf="enableCommentModeration">
<h2>{{ 'room-page.settings-direct-send' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="directSend" aria-labelledby="settings-direct-send"></mat-slide-toggle></h2>
<p>{{ 'room-page.settings-direct-send' | translate}}&nbsp;
<mat-slide-toggle [(ngModel)]="directSend" aria-labelledby="settings-direct-send"></mat-slide-toggle></p>
</div>
</div>
<div fxLayoutAlign="center center">
......
......@@ -24,16 +24,6 @@ mat-icon {
margin-right: 10px;
}
h1 {
margin: 10px 0 10px 0;
color: var(--on-surface);
}
h2 {
color: var(--on-surface);
font-weight: normal;
font-size: medium;
}
mat-divider {
margin-bottom: 10px;
......
......@@ -7,10 +7,12 @@ import { TranslateService } from '@ngx-translate/core';
import { RoomService } from '../../../../services/http/room.service';
import { Router } from '@angular/router';
import { CommentService } from '../../../../services/http/comment.service';
import { BonusTokenService } from '../../../../services/http/bonus-token.service';
import { CommentSettingsService } from '../../../../services/http/comment-settings.service';
import { DeleteCommentsComponent } from '../delete-comments/delete-comments.component';
import { CommentExportComponent } from '../comment-export/comment-export.component';
import { Room } from '../../../../models/room';
import { CommentBonusTokenMixin } from '../../../../models/comment-bonus-token-mixin';
import { CommentSettings } from '../../../../models/comment-settings';
import { CommentSettingsDialog } from '../../../../models/comment-settings-dialog';
......@@ -22,12 +24,14 @@ import { CommentSettingsDialog } from '../../../../models/comment-settings-dialo
export class CommentSettingsComponent implements OnInit {
roomId: string;
comments: Comment[];
comments: CommentBonusTokenMixin[];
commentThreshold = -100;
editRoom: Room;
settingThreshold = false;
enableCommentModeration = false;
directSend = true;
tagsEnabled = false;
tags: string[] = [];
constructor(
public dialogRef: MatDialogRef<RoomCreatorPageComponent>,
......@@ -38,19 +42,26 @@ export class CommentSettingsComponent implements OnInit {
public router: Router,
public commentService: CommentService,
public commentSettingsService: CommentSettingsService,
private bonusTokenService: BonusTokenService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
}
ngOnInit() {
if (this.editRoom.extensions && this.editRoom.extensions['comments']) {
if (this.editRoom.extensions['comments'].enableThreshold !== null) {
if (this.editRoom.extensions['comments'].commentThreshold) {
this.commentThreshold = this.editRoom.extensions['comments'].commentThreshold;
const commentExtension = this.editRoom.extensions['comments'];
if (commentExtension.enableThreshold !== null) {
if (commentExtension.commentThreshold) {
this.commentThreshold = commentExtension.commentThreshold;
} else {
this.commentThreshold = -100;
}
this.settingThreshold = this.editRoom.extensions['comments'].enableThreshold;
this.settingThreshold = commentExtension.enableThreshold;
}
if (commentExtension.enableTags !== null) {
this.tagsEnabled = commentExtension.enableTags;
this.tags = commentExtension.tags;
}
if (this.editRoom.extensions['comments'].enableModeration !== null) {
......@@ -75,6 +86,7 @@ export class CommentSettingsComponent implements OnInit {
const dialogRef = this.dialog.open(DeleteCommentsComponent, {
width: '400px'
});
dialogRef.componentInstance.roomId = this.roomId;
dialogRef.afterClosed()
.subscribe(result => {
if (result === 'delete') {
......@@ -90,38 +102,58 @@ export class CommentSettingsComponent implements OnInit {
this.commentService.deleteCommentsByRoomId(this.roomId).subscribe();
}
exportCsv(delimiter: string, date: string): void {
export(delimiter: string, date: string): void {
this.commentService.getAckComments(this.roomId)
.subscribe(comments => {
this.comments = comments;
const exportComments = JSON.parse(JSON.stringify(this.comments));
let csv: string;
let keyFields = '';
let valueFields = '';
keyFields = Object.keys(exportComments[0]).slice(3).join(delimiter) + '\r\n';
exportComments.forEach(element => {
element.body = '"' + element.body.replace(/[\r\n]/g, ' ').replace(/ +/g, ' ').replace(/"/g, '""') + '"';
valueFields += Object.values(element).slice(3).join(delimiter) + '\r\n';
this.bonusTokenService.getTokensByRoomId(this.roomId).subscribe( list => {
this.comments = comments.map(comment => {
const commentWithToken: CommentBonusTokenMixin = <CommentBonusTokenMixin>comment;
for (const bt of list) {
if (commentWithToken.creatorId === bt.userId && comment.id === bt.commentId) {
commentWithToken.bonusToken = bt.token;
}
}
return commentWithToken;
});
const exportComments = JSON.parse(JSON.stringify(this.comments));
let csv: string;
let valueFields = '';
const fieldNames = ['room-page.question', 'room-page.timestamp', 'room-page.presented',
'room-page.favorite', 'room-page.correct/wrong', 'room-page.score', 'room-page.token'];
let keyFields;
this.translationService.get(fieldNames).subscribe(msgs => {
keyFields = [msgs[fieldNames[0]], msgs[fieldNames[1]], msgs[fieldNames[2]], msgs[fieldNames[3]],
msgs[fieldNames[4]], msgs[fieldNames[5]], msgs[fieldNames[6]], '\r\n'];
exportComments.forEach(element => {
console.log(Object.values(element));
element.body = '"' + element.body.replace(/[\r\n]/g, ' ').replace(/ +/g, ' ').replace(/"/g, '""') + '"';
valueFields += Object.values(element).slice(3, 4) + delimiter;
let time;
time = Object.values(element).slice(4, 5);
valueFields += time[0].slice(0, 10) + '-' + time[0].slice(11, 16) + delimiter;
valueFields += Object.values(element).slice(5, 8) + delimiter;
valueFields += Object.values(element).slice(9, 11).join(delimiter) + '\r\n';
});
csv = keyFields + valueFields;
const myBlob = new Blob([csv], { type: 'text/csv' });
const link = document.createElement('a');
const fileName = this.editRoom.name + '_' + this.editRoom.shortId + '_' + date + '.csv';
link.setAttribute('download', fileName);
link.href = window.URL.createObjectURL(myBlob);
link.click();
});
});
csv = keyFields + valueFields;
const myBlob = new Blob([csv], { type: 'text/csv' });
const link = document.createElement('a');
const fileName = 'comments_' + date + '.csv';
link.setAttribute('download', fileName);
link.href = window.URL.createObjectURL(myBlob);
link.click();
});
}
onExport(exportType: string): void {
const date = new Date();
const dateString = date.getFullYear() + '_' + ('0' + (date.getMonth() + 1)).slice(-2) + '_' + ('0' + date.getDate()).slice(-2);
const timeString = ('0' + date.getHours()).slice(-2) + ('0' + date.getMinutes()).slice(-2) + ('0' + date.getSeconds()).slice(-2);
const timestamp = dateString + '_' + timeString;
const dateString = date.toLocaleDateString();
if (exportType === 'comma') {
this.exportCsv(',', timestamp);
this.export(',', dateString);
} else if (exportType === 'semicolon') {
this.exportCsv(';', timestamp);
this.export(';', dateString);
}
}
......@@ -135,7 +167,6 @@ export class CommentSettingsComponent implements OnInit {
}
closeDialog(): void {
console.log(this.commentThreshold);
const commentSettings = new CommentSettings();
commentSettings.roomId = this.roomId;
commentSettings.directSend = this.directSend;
......
......@@ -5,7 +5,7 @@
</mat-form-field>
<mat-form-field>
<textarea (focus)="eventService.makeFocusOnInputTrue()" (blur)="eventService.makeFocusOnInputFalse()"
matInput [(ngModel)]="content.body" rows="3" maxlength="255" placeholder="{{'content.body' | translate}}" name="body"></textarea>
matInput [(ngModel)]="content.body" rows="3" maxlength="250" placeholder="{{'content.body' | translate}}" name="body"></textarea>
</mat-form-field>
<h4>{{'content.answers' | translate}}</h4>
<mat-table [dataSource]="displayAnswers">
......
h3 {
color: var(--on-surface);
}
p {
color: var(--on-surface);
}
.delete {
background-color: var(--red);
color: var(--on-secondary);
......
......@@ -17,47 +17,28 @@ export class DeleteCommentComponent implements OnInit {
*/
confirmButtonType: DialogConfirmActionButtonType = DialogConfirmActionButtonType.Alert;
constructor(public dialogRef: MatDialogRef<RoomEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private liveAnnouncer: LiveAnnouncer,
private translationService: TranslateService ) { }
ngOnInit() {
this.announce();
}
close(type: string): void {
this.dialogRef.close(type);
}
public announce() {
const lang: string = this.translationService.currentLang;
// current live announcer content must be cleared before next read
this.liveAnnouncer.clear();
if (lang === 'de') {
this.liveAnnouncer.announce('Willst du die Frage wirklich löschen?');
} else {
this.liveAnnouncer.announce('Do you really want to delete this question');
}
this.translationService.get('comment-list.really-delete').subscribe(msg => {
this.liveAnnouncer.announce(msg);
});
}
/**
* Returns a lambda which closes the dialog on call.
*/
buildCloseDialogActionCallback(): () => void {
return () => this.close('abort');
return () => this.dialogRef.close('abort');
}
/**
* Returns a lambda which executes the dialog dedicated action on call.
*/
buildCommentDeleteActionCallback(): () => void {
return () => this.close('delete');
return () => this.dialogRef.close('delete');
}
}
<h1>{{ 'room-page.sure' | translate }}</h1>
<h2 *ngIf="!bonusQuestions">{{ 'room-page.sure' | translate }}</h2>
<h2 *ngIf="bonusQuestions">{{ 'room-page.delete-comments-alt-header' | translate }}</h2>
<mat-divider></mat-divider>
<p>{{ 'room-page.really-delete-comments' | translate }}</p>
<app-dialog-action-buttons
<p *ngIf="!bonusQuestions">{{ 'room-page.really-delete-comments' | translate }}</p>
<p *ngIf="bonusQuestions">{{ 'room-page.really-delete-comments-hint' | translate }}</p>
<app-dialog-action-buttons *ngIf="!bonusQuestions"
buttonsLabelSection="content"
confirmButtonLabel="delete"
[confirmButtonType]=confirmButtonType
[cancelButtonClickAction]="buildCloseDialogActionCallback()"
[confirmButtonClickAction]="buildCommentsDeleteActionCallback()"
></app-dialog-action-buttons>
<app-dialog-action-buttons *ngIf="bonusQuestions"
buttonsLabelSection="content"
[cancelButtonClickAction]="buildCloseDialogActionCallback()"
></app-dialog-action-buttons>
h1 {
color: var(--on-surface);
}
p {
color: var(--on-surface);
}
.delete {
background-color: var(--red);
color: var(--on-secondary);
......
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { RoomEditComponent } from '../room-edit/room-edit.component';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { DialogConfirmActionButtonType } from '../../../shared/dialog/dialog-action-buttons/dialog-action-buttons.component';
import { TranslateService } from '@ngx-translate/core';
import { BonusTokenService } from '../../../../services/http/bonus-token.service';
import { CommentSettingsComponent } from '../comment-settings/comment-settings.component';
@Component({
selector: 'app-delete-comment',
......@@ -16,33 +17,27 @@ export class DeleteCommentsComponent implements OnInit {
* The confirm button type of the dialog.
*/
confirmButtonType: DialogConfirmActionButtonType = DialogConfirmActionButtonType.Alert;
roomId: string;
bonusQuestions = false;
constructor(public dialogRef: MatDialogRef<RoomEditComponent>,
constructor(public dialogRef: MatDialogRef<CommentSettingsComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,