* settings

* display
* dashboard
* self-updater
* timers in app component
This commit is contained in:
Dávid Danyi 2018-04-13 18:17:11 +02:00
parent b0cbd691d5
commit e969edb26c
47 changed files with 1239 additions and 61 deletions

View File

@ -22,7 +22,9 @@
"../node_modules/semantic-ui-css/semantic.css",
"styles.css"
],
"scripts": [],
"scripts": [
"../node_modules/marked/lib/marked.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",

10
package-lock.json generated
View File

@ -290,6 +290,11 @@
"@types/jasmine": "2.8.6"
}
},
"@types/marked": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz",
"integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg=="
},
"@types/node": {
"version": "6.0.102",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.102.tgz",
@ -6836,6 +6841,11 @@
"object-visit": "1.0.1"
}
},
"marked": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz",
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg=="
},
"md5.js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",

View File

@ -21,7 +21,9 @@
"@angular/platform-browser": "^5.2.0",
"@angular/platform-browser-dynamic": "^5.2.0",
"@angular/router": "^5.2.0",
"@types/marked": "^0.3.0",
"core-js": "^2.4.1",
"marked": "^0.3.19",
"ng2-semantic-ui": "^0.9.7",
"rxjs": "^5.5.6",
"semantic-ui-css": "^2.3.1",

View File

@ -5,22 +5,57 @@ import { TeamListComponent} from './team-list/team-list.component';
import { TeamService } from '../shared/service/team.service';
import { TeamResolverService } from './team-resolver.service';
import { TeamEditorComponent } from './team-editor/team-editor.component';
import { SlideEditorComponent } from './slide-editor/slide-editor.component';
import { SlideListComponent } from './slide-list/slide-list.component';
import { SlideResolverService } from './slide-resolver.service';
import { SlideService } from '../shared/service/slide.service';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{
path: 'admin/teams/list',
path: 'admin',
component: DashboardComponent,
// canActivate: [AuthGuardService, RoleGuardService],
}, {
path: 'admin/teams',
component: TeamListComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
},
}
}, {
path: 'admin/team/new',
component: TeamEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
}, {
path: 'admin/team/edit/:id',
component: TeamEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
team: TeamResolverService,
},
}
}, {
path: 'admin/slides',
component: SlideListComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slides: SlideService,
}
}, {
path: 'admin/slide/new',
component: SlideEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
}
}, {
path: 'admin/slide/edit/:id',
component: SlideEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
teams: TeamService,
}
}
];

View File

@ -8,14 +8,20 @@ import { TeamEditorComponent } from './team-editor/team-editor.component';
import { SlideEditorComponent } from './slide-editor/slide-editor.component';
import { SlideListComponent } from './slide-list/slide-list.component';
import { TeamResolverService } from './team-resolver.service';
import { SlideResolverService } from './slide-resolver.service';
import { SuiModule } from 'ng2-semantic-ui';
import { DashboardComponent } from './dashboard/dashboard.component';
import { DisplayModule } from '../display/display.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
AdminRoutingModule
SuiModule,
AdminRoutingModule,
DisplayModule
],
declarations: [TeamListComponent, TeamEditorComponent, SlideEditorComponent, SlideListComponent],
providers: [TeamResolverService]
declarations: [TeamListComponent, TeamEditorComponent, SlideEditorComponent, SlideListComponent, DashboardComponent],
providers: [TeamResolverService, SlideResolverService]
})
export class AdminModule { }

View File

@ -0,0 +1,50 @@
<div class="ui main container">
<h1 class="ui dividing header">Dashboard</h1>
<div class="ui four cards">
<a class="ui raised yellow card" [routerLink]="['/commit-tracker']">
<div class="content">
<div class="header">Commit tracker</div>
<div class="meta">
<span class="category">Local</span>
</div>
<div class="description">
<p>View commits of the current configured team members.</p>
</div>
</div>
</a>
<a class="ui raised red card" [routerLink]="['/settings']">
<div class="content">
<div class="header">TV settings</div>
<div class="meta">
<span class="category">Local</span>
</div>
<div class="description">
<p>Change settings for this device, what team the device belongs to, and slideshow duration.</p>
</div>
</div>
</a>
<a class="ui raised green card" [routerLink]="['/admin/teams']">
<div class="content">
<div class="header">Teams</div>
<div class="meta">
<span class="category">Global</span>
</div>
<div class="description">
<p>Create teams, manage team members.</p>
</div>
</div>
</a>
<a class="ui raised blue card" [routerLink]="['/admin/slides']">
<div class="content">
<div class="header">Slides</div>
<div class="meta">
<span class="category">Global</span>
</div>
<div class="description">
<p>Create and edit slides for the TV, change slide visibility and assign the slides to individual teams</p>
</div>
</div>
</a>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
constructor(private titleService: Title) { }
ngOnInit() {
this.titleService.setTitle('Dashboard : MTAStv');
}
}

View File

@ -1,3 +1,54 @@
<p>
slide-editor works!
</p>
<div class="ui main container">
<h1 class="ui dividing header">Slide editor</h1>
<form class="ui form" #slideEditorForm (ngSubmit)="saveSlide()">
<div class="two fields">
<div class="eight wide field">
<label for="slide_name">Slide title</label>
<input id="slide_name" type="text" name="slide_name" [(ngModel)]="slide.title">
</div>
<div class="eight wide field">
<label for="team">Visible to this team</label>
<sui-select class="selection"
id="team"
name="team"
[(ngModel)]="slide.team"
labelField="name"
[isSearchable]="true"
#select>
<sui-select-option [value]="emptyTeam"></sui-select-option>
<sui-select-option *ngFor="let team of teams" [value]="team"></sui-select-option>
</sui-select>
</div>
</div>
<div class="field">
<label for="slide_data">Slide data</label>
<textarea id="slide_data" rows="30" name="slide_data" [(ngModel)]="slide.slideData"></textarea>
</div>
<div class="five wide field">
<div class="ui checkbox">
<input type="checkbox" id="mustLowerCost" name="mustLowerCost"
[(ngModel)]="slide.isVisible">
<label for="mustLowerCost">Active</label>
</div>
</div>
<button type="submit" class="ui button"
[class.positive]="canSave"
[class.disabled]="!canSave">Save</button>
<button type="button" class="ui button"
[class.primary]="canPreview"
[class.disabled]="!canPreview"
(click)="preview()"><i class="search icon"></i>Preview</button>
<a class="ui button"
[routerLink]="['/admin/slides']"><i class="left angle icon"></i>Back to slides list</a>
</form>
<app-slide [data]="renderedPreview"
[preview]="true"
[(visible)]="previewVisible"
*ngIf="previewVisible"></app-slide>
</div>

View File

@ -1,4 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import * as marked from 'marked';
import { Slide } from '../../shared/slide';
import { SlideService } from '../../shared/service/slide.service';
import { Team } from '../../shared/team';
@Component({
selector: 'app-slide-editor',
@ -6,10 +13,56 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./slide-editor.component.css']
})
export class SlideEditorComponent implements OnInit {
private md;
public emptyTeam: Team = new Team();
public slide: Slide;
public teams: Array<Team> = [];
public renderedPreview: String = '';
public previewVisible = false;
constructor() { }
ngOnInit() {
constructor(private slideService: SlideService,
private titleService: Title,
private route: ActivatedRoute,
private router: Router) {
this.md = marked.setOptions({});
this.emptyTeam.name = 'All teams';
}
ngOnInit() {
this.titleService.setTitle('Edit slide : MTAStv');
this.route.data.subscribe((data: {
slide: Slide,
teams: Array<Team>
}) => {
this.teams = data.teams;
this.slide = data.slide ? data.slide : new Slide;
this.slide.team = this.slide.team === null
? this.emptyTeam
: this.teams.find(team => team.id === this.slide.team.id);
});
}
public saveSlide() {
if (this.canSave) {
this.slideService
.persist(this.slide)
.subscribe(result => this.router.navigate(['/admin/slides']));
}
}
get canSave(): boolean {
return [
this.slide.title,
this.slide.slideData
].every(field => field.trim().length > 0);
}
get canPreview(): boolean {
return this.slide.slideData.trim().length > 0;
}
public preview() {
this.previewVisible = true;
this.renderedPreview = this.md.parse(this.slide.slideData);
}
}

View File

@ -1,3 +1,30 @@
<p>
slide-list works!
</p>
<div class="ui main container">
<h1 class="ui dividing header">Slides</h1>
<a class="ui primary button"
[routerLink]="['/admin/slide/new']"><i class="plus sign icon"></i>New slide</a>
<a class="ui button"
[routerLink]="['/admin']"><i class="left angle icon"></i>Back to dashboard</a>
<table *ngIf="slides?.length" class="ui large padded celled definition table">
<thead>
<tr>
<th class="collapsing"></th>
<th><i class="large address book outline icon"></i>Slide title</th>
<th class="collapsing"><i class="large users icon"></i>Owner team</th>
<th class="collapsing"><i class="large check square outline icon"></i>Visible</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let slide of slides">
<td class="collapsing">
<a [routerLink]="['/admin/slide/edit', slide.id]" title="Change"><i
class="large pencil alternate icon"></i></a>
<a title="Delete" (click)="delete(slide)"><i
class="large red fitted trash alternate outline icon"></i></a>
</td>
<td>{{slide.title}}</td>
<td class="collapsing">{{slideTeam(slide.team)}}</td>
<td class="center aligned"><i class="large icon" [ngClass]="visibleClass(slide)"></i></td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,4 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { SlideService } from '../../shared/service/slide.service';
import { Slide } from '../../shared/slide';
import { Team } from '../../shared/team';
@Component({
selector: 'app-slide-list',
@ -7,9 +13,42 @@ import { Component, OnInit } from '@angular/core';
})
export class SlideListComponent implements OnInit {
constructor() { }
ngOnInit() {
constructor(private slideService: SlideService,
private titleService: Title,
private route: ActivatedRoute) {
}
ngOnInit() {
this.titleService.setTitle('Slides : MTAStv');
this.route.data.subscribe((data: {slides: Array<Slide>}) => this.slides = data.slides);
}
get slides(): Array<Slide> {
return this.slideService.slides;
}
set slides(slides: Array<Slide>) {
this.slideService.slides = slides;
}
public slideTeam(team: Team): String {
return team === null ? 'All teams' : team.name;
}
public delete(slide: Slide) {
if (confirm(`Are you sure you want to delete the slide '${slide.title}'`)) {
this.slideService.delete(slide).subscribe(result => {
if (result) {
this.slides = this.slides.filter(slideItem => slideItem !== slide);
}
});
}
}
public visibleClass(slide: Slide) {
return {
'green check': slide.isVisible,
'red times': !slide.isVisible
};
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { SlideResolverService } from './slide-resolver.service';
describe('SlideResolverService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SlideResolverService]
});
});
it('should be created', inject([SlideResolverService], (service: SlideResolverService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../environments/environment';
import { Slide } from '../shared/slide';
@Injectable()
export class SlideResolverService implements Resolve<Slide> {
private apiEndPoint = environment.apiUrl + '/api/slide';
constructor(private httpClient: HttpClient) {}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Slide> {
return this.getSlide(route.params['id']).toPromise();
}
public getSlide(id: Number): Observable<Slide> {
return this.httpClient.get<Slide>(`${this.apiEndPoint}/${id}`);
}
}

View File

@ -1,12 +1,18 @@
<div class="ui main container">
<h1 class="ui dividing header">Team editor</h1>
<form class="ui form" #teamEditorForm (ngSubmit)="saveTeam()">
<div class="six wide field">
<label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name" [(ngModel)]="team.name">
</div>
<button type="submit" class="ui positive button">Save changes</button>
<div class="six wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive">
<label for="team_is_active">Active</label>
</div>
</div>
<h4 class="ui dividing header">Team members</h4>
<div class="three inline fields">
@ -14,24 +20,27 @@
<button type="button" class="ui fluid button"
[class.positive]="canAddMember"
[class.disabled]="!canAddMember"
(keydown.enter)="handleEnter($event)"
(click)="addMember()">Add
</button>
</div>
<div class="five wide field">
<input type="text"
<input type="text" #signumInput
name="member_signum"
placeholder="Signum"
(keydown.enter)="handleEnter($event)"
[(ngModel)]="member.signum">
</div>
<div class="nine wide field">
<input type="text"
name="member_name"
placeholder="Display name"
(keydown.enter)="handleEnter($event)"
[(ngModel)]="member.name">
</div>
</div>
<h4 class="ui dividing header"></h4>
<table class="ui celled definition table">
<table class="ui celled definition table" *ngIf="team.members.length">
<thead>
<tr>
<th class="collapsing"></th>
@ -49,5 +58,11 @@
</tbody>
</table>
<button type="submit" class="ui button"
[class.positive]="canSave"
[class.disabled]="!canSave"><i class="save outline icon"></i>Save changes
</button>
<a class="ui button"
[routerLink]="['/admin/teams']"><i class="left angle icon"></i>Back to teams list</a>
</form>
</div>

View File

@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component, ElementRef, HostBinding, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TeamService } from '../../shared/service/team.service';
import { Team } from '../../shared/team';
import { Member } from '../../shared/member';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
@Component({
selector: 'app-team-editor',
@ -12,7 +13,8 @@ import { Member } from '../../shared/member';
styleUrls: ['./team-editor.component.css']
})
export class TeamEditorComponent implements OnInit {
public team: Team = new Team();
@ViewChild('signumInput') signumInputElement: ElementRef;
public team: Team;
public member: Member = new Member();
constructor(private teamService: TeamService,
@ -23,31 +25,55 @@ export class TeamEditorComponent implements OnInit {
ngOnInit() {
this.titleService.setTitle('Team editor : MTAStv');
this.route.data.subscribe((data: { team: Team }) => this.team = data.team);
this.route.data.subscribe((data: { team: Team }) => this.team = data.team ? data.team : new Team());
}
get canAddMember(): boolean {
try {
return [this.member.name, this.member.signum].every(field => field.length !== 0);
return [this.member.name, this.member.signum].every(field => field.length !== 0)
&& this.team.members.every(member => member.signum !== this.member.signum);
} catch (e) {
return false;
}
}
public addMember() {
this.team.members = this.team.members.concat(Object.assign({}, this.member));
this.team.members = this.team.members
.concat(Object.assign({}, this.member))
.sort((a: Member, b: Member) => a.signum < b.signum ? -1 : 1);
this.member = new Member();
}
public handleEnter(ev: KeyboardEvent) {
ev.preventDefault();
if (this.canAddMember) {
this.addMember();
this.focusSignumField();
}
}
public focusSignumField() {
this.signumInputElement.nativeElement.focus();
}
public removeMember(signum: String) {
if (confirm(`Remove the member with signum ${signum}?`)) {
this.team.members = this.team.members.filter(member => member.signum !== signum);
}
}
get canSave(): boolean {
return [
this.team.name.trim(),
this.team.members
].every(field => field.length > 0);
}
public saveTeam() {
this.teamService.update(this.team).subscribe(
() => this.router.navigate(['/admin/teams/list'])
);
if (this.canSave) {
this.teamService.persist(this.team).subscribe(
() => this.router.navigate(['/admin/teams'])
);
}
}
}

View File

@ -1,22 +1,30 @@
<div class="ui main container">
<h1 class="ui dividing header">Teams</h1>
<a class="ui primary button"
[routerLink]="['/admin/team/new']"><i class="plus sign icon"></i>New team</a>
<a class="ui button"
[routerLink]="['/admin']"><i class="left angle icon"></i>Back to dashboard</a>
<table *ngIf="teams?.length" class="ui large padded celled definition table">
<thead>
<tr>
<th></th>
<th class="clickable"><i class="large address book outline icon"></i>Team</th>
<th class="clickable"><i class="large users icon"></i>Members</th>
<th class="collapsing"></th>
<th><i class="large address book outline icon"></i>Team</th>
<th><i class="large users icon"></i>Members</th>
<th class="collapsing"><i class="large check square outline icon"></i>Active</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let team of teams">
<td class="collapsing">
<a [routerLink]="['/admin/team/edit', team.id]" title="Change"><i
class="large fitted pencil alternate icon"></i></a>
class="large pencil alternate icon"></i></a>
<a title="Delete" (click)="delete(team)"><i
class="large red fitted trash alternate outline icon"></i></a>
</td>
<td>{{team.name}}</td>
<td>{{fancyMemberNames(team)}}</td>
<td class="center aligned"><i class="large icon" [ngClass]="activeClass(team)"></i></td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { Team } from '../../shared/team';
import { TeamService } from '../../shared/service/team.service';
import { Team } from '../../shared/team';
@Component({
selector: 'app-team-list',
@ -33,4 +33,21 @@ export class TeamListComponent implements OnInit {
public fancyMemberNames(team: Team): String {
return team.members.map(member => member.name).join(', ');
}
public delete(team: Team) {
if (confirm(`Remove the team '${team.name}'?`)) {
this.teamService.delete(team).subscribe(result => {
if (result) {
this.teams = this.teams.filter(teamItem => teamItem !== team);
}
});
}
}
public activeClass(team: Team) {
return {
'green check': team.isActive,
'red times': !team.isActive
};
}
}

View File

@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Team } from '../shared/team';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class TeamResolverService implements Resolve<Team> {

View File

@ -1,10 +1,70 @@
import { Component } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { CommitTrackerService } from './shared/service/commit-tracker.service';
import { SettingsService } from './shared/service/settings.service';
import { SelfUpdaterService } from './shared/service/self-updater.service';
import { ActivationEnd, Router } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import { TimerObservable } from 'rxjs/observable/TimerObservable';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/switchMap';
const TIMER_UPDATE_POLL_INTERVAL = 30000;
const TIMER_COMMITTRACKER_REFRESH = 5000;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
export class AppComponent implements OnInit, OnDestroy {
private selfUpdateCheckerTimer: Subscription;
private refreshCommitTrackerTimer: Subscription;
private slideshowTimer: Subscription;
private autoSwitch = false;
constructor(private commitTrackerService: CommitTrackerService,
private settings: SettingsService,
private selfUpdaterService: SelfUpdaterService,
private router: Router) {}
public ngOnInit() {
const timerSUC = TimerObservable.create(TIMER_UPDATE_POLL_INTERVAL, TIMER_UPDATE_POLL_INTERVAL);
this.selfUpdateCheckerTimer = timerSUC.subscribe(() => {
this.selfUpdaterService.checkAndReloadIfNecessary();
});
const timerCT = TimerObservable.create(TIMER_COMMITTRACKER_REFRESH, TIMER_COMMITTRACKER_REFRESH);
this.refreshCommitTrackerTimer = timerCT.subscribe(() => {
this.commitTrackerService.getTeamCommits(this.settings.team.members.map(member => member.signum));
});
const timerSS = new Subject();
this.slideshowTimer = timerSS.switchMap(period => TimerObservable.create(period))
.subscribe(() => {
this.changeSlide(timerSS);
});
timerSS.next(this.settings.slideInterval);
this.autoSwitch = false;
this.router.events
.filter(event => event instanceof ActivationEnd)
.subscribe(event => {
this.autoSwitch = !!event.snapshot.data.autoSwitchable;
});
}
public ngOnDestroy() {
this.refreshCommitTrackerTimer.unsubscribe();
this.selfUpdateCheckerTimer.unsubscribe();
this.slideshowTimer.unsubscribe();
}
private changeSlide(timer: Subject) {
if (this.autoSwitch) {
console.log('Slide should have changed here');
}
timer.next(this.settings.slideInterval);
}
}

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { SuiModule } from 'ng2-semantic-ui';
@ -11,6 +12,8 @@ import { SlideService } from './shared/service/slide.service';
import { AdminModule } from './admin/admin.module';
import { DisplayModule } from './display/display.module';
import { CommitTrackerService } from './shared/service/commit-tracker.service';
import { SettingsService } from './shared/service/settings.service';
import { SelfUpdaterService } from './shared/service/self-updater.service';
@NgModule({
declarations: [
@ -18,6 +21,7 @@ import { CommitTrackerService } from './shared/service/commit-tracker.service';
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
FormsModule,
SuiModule,
@ -25,7 +29,7 @@ import { CommitTrackerService } from './shared/service/commit-tracker.service';
AdminModule,
AppRoutingModule, // must be last RouterModule import for ** route to work
],
providers: [TeamService, SlideService, CommitTrackerService],
providers: [TeamService, SlideService, CommitTrackerService, SettingsService, SelfUpdaterService],
bootstrap: [AppComponent]
})
export class AppModule {

View File

@ -3,15 +3,39 @@ import { Routes, RouterModule } from '@angular/router';
import { CommitTrackerComponent } from './commit-tracker/commit-tracker.component';
import { CommitTrackerService } from '../shared/service/commit-tracker.service';
import { SettingsComponent } from './settings/settings.component';
import { TeamService } from '../shared/service/team.service';
import { SlideShowComponent } from './slide-show/slide-show.component';
import { SlideResolverService } from '../admin/slide-resolver.service';
const routes: Routes = [
{
path: 'slideshow/:id',
component: SlideShowComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
},
data: {
autoSwitchable: true
}
}, {
path: 'commit-tracker',
component: CommitTrackerComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
commits: CommitTrackerService,
},
data: {
autoSwitchable: true
}
}, {
path: 'settings',
component: SettingsComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
},
}
];

View File

@ -1,14 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DisplayRoutingModule } from './display-routing.module';
import { CommitTrackerComponent } from './commit-tracker/commit-tracker.component';
import { SettingsComponent } from './settings/settings.component';
import { SuiModule } from 'ng2-semantic-ui';
import { SlideComponent } from './slide/slide.component';
import { SlideShowComponent } from './slide-show/slide-show.component';
@NgModule({
imports: [
CommonModule,
FormsModule,
SuiModule,
DisplayRoutingModule
],
declarations: [CommitTrackerComponent]
exports: [SlideComponent],
declarations: [CommitTrackerComponent, SettingsComponent, SlideComponent, SlideShowComponent]
})
export class DisplayModule { }

View File

@ -0,0 +1,30 @@
<div class="ui main container">
<h1 class="ui dividing header">TV settings</h1>
<form class="ui form" #teamSelectorForm (ngSubmit)="saveSettings()">
<div class="two fields">
<div class="six wide field">
<label for="team">Team name</label>
<sui-select class="selection"
id="team"
name="team"
[(ngModel)]="selectedTeam"
[options]="options"
labelField="name"
[isSearchable]="false"
#select>
<sui-select-option *ngFor="let team of teams" [value]="team"></sui-select-option>
</sui-select>
</div>
<div class="four wide field">
<label for="slide_interval">Slide duration: {{slideInterval}}ms</label>
<input id="slide_interval" name="slide_interval"
type="range" min="1000" max="120000" step="100"
[(ngModel)]="slideInterval">
</div>
</div>
<button type="submit" class="ui positive button"><i class="save outline icon"></i>Save</button>
<a class="ui button"
[routerLink]="['/admin']"><i class="left angle icon"></i>Back to dashboard</a>
</form>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SettingsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { Team } from '../../shared/team';
import { ActivatedRoute, Router } from '@angular/router';
import { SettingsService } from '../../shared/service/settings.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.css']
})
export class SettingsComponent implements OnInit {
public teams: Array<Team> = [];
public selectedTeam: Team;
public slideInterval: Number;
constructor(private route: ActivatedRoute,
private settingsService: SettingsService,
private router: Router) {}
ngOnInit() {
this.route.data.subscribe((data: {teams: Array<Team>}) => {
this.teams = data.teams;
this.selectedTeam = this.teams.find(team => team.id === this.settingsService.team.id);
this.slideInterval = this.settingsService.slideInterval;
});
}
public saveSettings() {
this.settingsService.team = this.selectedTeam;
this.settingsService.slideInterval = this.slideInterval;
this.router.navigate(['/admin']);
}
}

View File

@ -0,0 +1 @@
<app-slide [data]="renderedSlide"></app-slide>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SlideShowComponent } from './slide-show.component';
describe('SlideShowComponent', () => {
let component: SlideShowComponent;
let fixture: ComponentFixture<SlideShowComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SlideShowComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SlideShowComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, HostBinding, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import * as marked from 'marked';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
import { Slide } from '../../shared/slide';
@Component({
selector: 'app-slide-show',
templateUrl: './slide-show.component.html',
styleUrls: ['./slide-show.component.css'],
animations: [slideInOutAnimation]
})
export class SlideShowComponent implements OnInit {
private md;
public slide: Slide;
@HostBinding('@slideInOutAnimation')
get slideIn() {
return '';
}
constructor(private route: ActivatedRoute,
private titleService: Title) {
this.md = marked.setOptions({});
}
ngOnInit() {
this.route.data.subscribe((data: {slide: Slide}) => {
this.slide = data.slide;
this.titleService.setTitle(`${this.slide.title} : MTAStv`);
});
}
get renderedSlide(): String {
return this.md.parse(this.slide.slideData);
}
}

View File

@ -0,0 +1,244 @@
:host {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background: #222;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-size: 42px;
font-weight: normal;
color: #fff; }
section {
line-height: 1.3;
font-weight: inherit; }
/*********************************************
* HEADERS
*********************************************/
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 20px 0;
color: #fff;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-weight: 600;
line-height: 1.2;
letter-spacing: normal;
text-transform: uppercase;
text-shadow: none;
word-wrap: break-word; }
h1 {
font-size: 2.5em; }
h2 {
font-size: 1.6em; }
h3 {
font-size: 1.3em; }
h4 {
font-size: 1em; }
h1 {
text-shadow: none; }
/*********************************************
* OTHER
*********************************************/
p {
margin: 20px 0;
line-height: 1.3; }
/* Ensure certain elements are never larger than the slide itself */
img,
video,
iframe {
max-width: 95%;
max-height: 95%; }
strong,
b {
font-weight: bold; }
em {
font-style: italic; }
ol,
dl,
ul {
display: inline-block;
text-align: left;
margin: 0 0 0 1em; }
ol {
list-style-type: decimal; }
ul {
list-style-type: disc; }
ul ul {
list-style-type: square; }
ul ul ul {
list-style-type: circle; }
ul ul,
ul ol,
ol ol,
ol ul {
display: block;
margin-left: 40px; }
dt {
font-weight: bold; }
dd {
margin-left: 40px; }
blockquote {
display: block;
position: relative;
width: 70%;
margin: 20px auto;
padding: 5px;
font-style: italic;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); }
blockquote p:first-child,
blockquote p:last-child {
display: inline-block; }
q {
font-style: italic; }
pre {
display: block;
position: relative;
width: 90%;
margin: 20px auto;
text-align: left;
font-size: 0.55em;
font-family: monospace;
line-height: 1.2em;
word-wrap: break-word;
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); }
code {
font-family: monospace;
text-transform: none; }
pre code {
display: block;
padding: 5px;
overflow: auto;
max-height: 400px;
word-wrap: normal; }
table {
margin: auto;
border-collapse: collapse;
border-spacing: 0; }
table th {
font-weight: bold; }
table th,
table td {
text-align: left;
padding: 0.2em 0.5em 0.2em 0.5em;
border-bottom: 1px solid; }
table th[align="center"],
table td[align="center"] {
text-align: center; }
table th[align="right"],
table td[align="right"] {
text-align: right; }
table tbody tr:last-child th,
table tbody tr:last-child td {
border-bottom: none; }
sup {
vertical-align: super; }
sub {
vertical-align: sub; }
small {
display: inline-block;
font-size: 0.6em;
line-height: 1.2em;
vertical-align: top; }
small * {
vertical-align: top; }
/*********************************************
* LINKS
*********************************************/
a {
color: #42affa;
text-decoration: none;
-webkit-transition: color .15s ease;
-moz-transition: color .15s ease;
transition: color .15s ease; }
a:hover {
color: #8dcffc;
text-shadow: none;
border: none; }
.roll span:after {
color: #fff;
background: #068de9; }
/*********************************************
* IMAGES
*********************************************/
section img {
margin: 15px 0px;
background: rgba(255, 255, 255, 0.12);
border: 4px solid #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); }
section img.plain {
border: 0;
box-shadow: none; }
a img {
-webkit-transition: all .15s linear;
-moz-transition: all .15s linear;
transition: all .15s linear; }
a:hover img {
background: rgba(255, 255, 255, 0.2);
border-color: #42affa;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); }
/*********************************************
* NAVIGATION CONTROLS
*********************************************/
.controls {
color: #42affa; }
/*********************************************
* PROGRESS BAR
*********************************************/
.progress {
background: rgba(0, 0, 0, 0.2);
color: #42affa; }
.progress span {
-webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
-moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }

View File

@ -0,0 +1 @@
<div class="present" [innerHTML]="data"></div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SlideComponent } from './slide.component';
describe('SlideComponent', () => {
let component: SlideComponent;
let fixture: ComponentFixture<SlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SlideComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SlideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-slide',
templateUrl: './slide.component.html',
styleUrls: ['./slide.component.css']
})
export class SlideComponent implements OnInit {
@Input() data: String = '';
@Input() preview = false;
@Input() visible = true;
@Output() visibleChange = new EventEmitter();
constructor() {}
ngOnInit() {}
@HostListener('click')
public hide() {
if (this.preview) {
this.visibleChange.emit(false);
}
}
}

View File

@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../../environments/environment';
import { Commit } from '../commit';
import { TeamService } from './team.service';
import { SettingsService } from './settings.service';
@Injectable()
export class CommitTrackerService implements Resolve<Array<Commit>> {
@ -14,10 +14,13 @@ export class CommitTrackerService implements Resolve<Array<Commit>> {
private cachedCommits: Array<Commit> = [];
constructor(private httpClient: HttpClient,
private teamService: TeamService) { }
private settingsService: SettingsService) { }
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Array<Commit>> {
return this.getTeamCommits(this.teamService.teamMembers).toPromise();
const signums = this.settingsService.team
? this.settingsService.team.members.map(member => member.signum)
: [];
return this.getTeamCommits(signums).toPromise();
}
public getTeamCommits(signums: Array<String>): Observable<Array<Commit>> {

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { SelfUpdaterService } from './self-updater.service';
describe('SelfUpdaterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SelfUpdaterService]
});
});
it('should be created', inject([SelfUpdaterService], (service: SelfUpdaterService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '@angular/common';
@Injectable()
export class SelfUpdaterService {
private appRevision: Number = 0;
private initFailed = false;
constructor(private httpClient: HttpClient,
private locationService: Location) {
this.getDeployedRevision().subscribe(
revision => this.appRevision = revision,
() => {
console.log(
'%c Couldn\'t load initial revision data from server. Self update disabled.',
'background: #222; color: #bada55;'
);
this.initFailed = true;
}
);
}
private getDeployedRevision(): Observable<Number> {
return this.httpClient.get<Number>(this.locationService.prepareExternalUrl('/revision.json'));
}
public checkAndReloadIfNecessary() {
if (!this.initFailed) {
this.getDeployedRevision().subscribe(revision => {
if (revision > this.appRevision) {
this.locationService.go('/');
}
});
}
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { SettingsService } from './settings.service';
describe('SettingsService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SettingsService]
});
});
it('should be created', inject([SettingsService], (service: SettingsService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { Team } from '../team';
const DEFAULT_SLIDE_INTERVAL = 30000;
const SLIDE_INTERVAL_KEY = 'slide_interval';
const SELECTED_TEAM_KEY = 'team';
@Injectable()
export class SettingsService {
constructor() {
}
get team(): Team {
try {
const team = JSON.parse(localStorage.getItem(SELECTED_TEAM_KEY));
return team !== null ? team : new Team;
} catch (e) {
return new Team;
}
}
set team(team: Team) {
localStorage.setItem(SELECTED_TEAM_KEY, JSON.stringify(team));
}
get slideInterval(): Number {
try {
const interval = JSON.parse(localStorage.getItem(SLIDE_INTERVAL_KEY));
return interval !== null ? interval : DEFAULT_SLIDE_INTERVAL;
} catch (e) {
return DEFAULT_SLIDE_INTERVAL;
}
}
set slideInterval(interval: Number) {
localStorage.setItem(SLIDE_INTERVAL_KEY, JSON.stringify(interval));
}
}

View File

@ -1,8 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../../environments/environment';
import { Slide } from '../slide';
@Injectable()
export class SlideService {
export class SlideService implements Resolve<Array<Slide>>{
constructor() { }
private apiEndPoint = environment.apiUrl + '/api/slide';
private cachedSlides: Array<Slide> = [];
constructor(private httpClient: HttpClient) {}
private static prepareSlideData(slide: Slide) {
const slideToSave = <any>Object.assign({}, slide);
slideToSave.team = slideToSave.team.id === null
? null
: slideToSave.team.id;
return slideToSave;
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Array<Slide>> {
return this.list().toPromise();
}
public list(): Observable<Array<Slide>> {
return this.httpClient.get<Array<Slide>>(this.apiEndPoint);
}
public persist(slide: Slide): Observable<Slide> {
return slide.id === null
? this.create(slide)
: this.update(slide);
}
public create(slide: Slide): Observable<Slide> {
return this.httpClient.post<Slide>(this.apiEndPoint, SlideService.prepareSlideData(slide));
}
public update(slide: Slide): Observable<Slide> {
return this.httpClient.put<Slide>(`${this.apiEndPoint}/${slide.id.toString()}`, SlideService.prepareSlideData(slide));
}
public delete(slide: Slide): Observable<boolean> {
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${slide.id.toString()}`);
}
get slides(): Array<Slide> {
return this.cachedSlides;
}
set slides(slides: Array<Slide>) {
this.cachedSlides = slides;
}
}

View File

@ -22,6 +22,12 @@ export class TeamService implements Resolve<Array<Team>> {
return this.httpClient.get<Array<Team>>(this.apiEndPoint);
}
public persist(team: Team): Observable<Team> {
return team.id === null
? this.create(team)
: this.update(team);
}
public create(team: Team) {
return this.httpClient.post<Team>(this.apiEndPoint, team);
}
@ -41,8 +47,4 @@ export class TeamService implements Resolve<Array<Team>> {
set teams(teams: Array<Team>) {
this.cachedTeams = teams;
}
get teamMembers(): Array<string> {
return ['eztoli', 'etamecs', 'esteist', 'emrtsis', 'erudvel'];
}
}

View File

@ -0,0 +1,43 @@
// import the required animation functions from the angular animations module
import { animate, state, style, transition, trigger } from '@angular/animations';
export const slideInOutAnimation =
// trigger name for attaching this animation to an element using the [@triggerName] syntax
trigger('slideInOutAnimation', [
// end state styles for route container (host)
state('*', style({
// the view covers the whole screen with a semi tranparent background
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0
})),
// route 'enter' transition
transition(':enter', [
// styles at start of transition
style({
// start with the content positioned off the right of the screen,
// -400% is required instead of -100% because the negative position adds to the width of the element
right: '-400%'
}),
// animation and styles at end of transition
animate('.5s ease-in-out', style({
// transition the right position to 0 which slides the content into view
right: 0
}))
]),
// route 'leave' transition
transition(':leave', [
// animation and styles at end of transition
animate('.5s ease-in-out', style({
// transition the right position to -400% which slides the content out of view
right: '-400%'
}))
])
]);

View File

@ -1,11 +1,11 @@
import { Team } from './team';
export class Slide {
id: Number;
title: String;
team: Team;
slideData: String;
isVisible: boolean;
createdAt: String;
updatedAt: String;
id: Number = null;
title: String = '';
team: Team = null;
slideData: String = '';
isVisible = true;
createdAt: String = null;
updatedAt: String = null;
}

View File

@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */
html,
body {
height: calc(100% - 2em);
}
.main.container {
margin-top: 2em;
}