Reorganize app files, setup initial routing and auth checks

Log in/out is functional with notifications (modified from recipes project).
Shared resources are now organized as such, with barrels to make imports simple.
This commit is contained in:
kiswa 2016-07-16 12:09:31 +00:00
parent ea7f18ef7b
commit 68d693307d
37 changed files with 533 additions and 133 deletions

View File

@ -32,6 +32,7 @@
"@angular/common": "^2.0.0-rc.4",
"@angular/compiler": "^2.0.0-rc.4",
"@angular/core": "^2.0.0-rc.4",
"@angular/forms": "^0.2.0",
"@angular/http": "^2.0.0-rc.4",
"@angular/platform-browser": "^2.0.0-rc.4",
"@angular/platform-browser-dynamic": "^2.0.0-rc.4",

View File

@ -5,3 +5,11 @@ SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript text/javascript
</IfModule>
RewriteEngine On
RewriteCond %{REQUEST_URI}::$1 ^(.*?/)(.*)::\2$
RewriteRule ^(.*)$ - [E=BASE:%1]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ %{ENV:BASE}index.html [QSA,L]

View File

@ -124,8 +124,13 @@ class Auth extends BaseController {
$user->last_login = time();
$user->save();
$user->security_level = $user->security_level->getValue();
unset($user->password_hash);
unset($user->active_token);
$this->apiJson->setSuccess();
$this->apiJson->addData($jwt);
$this->apiJson->addData($user);
return $this->jsonResponse($response);
}

View File

@ -1,2 +1,4 @@
<router-outlet></router-outlet>
<tb-notifications></tb-notifications>

View File

@ -1,9 +1,11 @@
import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES } from '@angular/router';
import { Notifications } from './shared/index';
@Component({
selector: 'app-component',
directives: [ ROUTER_DIRECTIVES ],
directives: [ ROUTER_DIRECTIVES, Notifications ],
templateUrl: 'app/app.component.html'
})
export class AppComponent {

View File

@ -1,16 +1,36 @@
import { RouterConfig, provideRouter } from '@angular/router';
import { AuthGuard, AuthService } from './shared/index';
import { Login } from './login/login.component';
import { Board } from './board/board.component';
import { Settings } from './settings/settings.component';
import { Dashboard } from './dashboard/dashboard.component';
const ROUTES: RouterConfig = [
{
path: '',
component: Login
},
{
path: 'boards',
component: Board,
canActivate: [ AuthGuard ]
},
{
path: 'settings',
component: Settings,
canActivate: [ AuthGuard ]
},
{
path: 'dashboard',
component: Dashboard,
canActivate: [ AuthGuard ]
}
];
export const APP_ROUTER_PROVIDERS = [
provideRouter(ROUTES)
provideRouter(ROUTES),
AuthGuard,
AuthService
];

View File

@ -38,45 +38,53 @@
<span class="right icon icon-angle-double-down"
title="Collapse Column"></span>
</h3>
<div class="task">
<h4>
<span class="icon icon-minus-squared-alt" title="Collapse Task"></span>
Something to Get Done
<span class="badge right" title="Points">3</span>
</h4>
<div class="description">
<p>This is the thing that needs to get done.</p>
<div class="tasks">
<div class="task">
<h4>
<span class="icon icon-minus-squared-alt" title="Collapse Task"></span>
Something to Get Done
<span class="badge right" title="Points">3</span>
</h4>
<div class="description">
<p>This is the thing that needs to get done.</p>
</div>
<div class="stats">
Assigned To: admin
<span class="right">
<span class="icon icon-chat-empty" title="4 Comments"></span>
<span class="icon icon-attach" title="2 Attachments"></span>
<span class="category" title="Category"></span>
</span>
</div>
</div>
<div class="stats">
Assigned To: admin
<span class="right">
<span class="icon icon-chat-empty" title="4 Comments"></span>
<span class="icon icon-attach" title="2 Attachments"></span>
<span class="category" title="Category"></span>
</span>
<div class="task">
<h4>
<span class="icon icon-minus-squared-alt" title="Collapse Task"></span>
A Task With a Very Long Title to Test How it Wraps
<span class="badge right" title="Points">3</span>
</h4>
<div class="description">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Fusce ut commodo urna, vitae lacinia quam. Cras eleifend magna massa, vitae porttitor dolor lacinia quis.</p>
<p>Nam tortor risus, faucibus nec lacinia ac, laoreet vitae diam.</p>
<p>Nullam viverra elementum mi, at tincidunt nisl pretium a. Fusce risus risus, ornare facilisis leo a, dictum porta nibh.</p>
<p>Quisque ut augue tortor. Phasellus hendrerit placerat molestie.</p>
</div>
<div class="stats">
Assigned To: admin
<span class="right">
<span class="icon icon-chat-empty" title="4 Comments"></span>
<span class="icon icon-attach" title="2 Attachments"></span>
<span class="category" title="Category">Front End</span>
</span>
</div>
</div>
</div>
<div class="task">
<h4>
<span class="icon icon-minus-squared-alt" title="Collapse Task"></span>
A Task With a Very Long Title to Test How it Wraps
<span class="badge right" title="Points">3</span>
</h4>
<div class="description">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Fusce ut commodo urna, vitae lacinia quam. Cras eleifend magna massa, vitae porttitor dolor lacinia quis.</p>
<p>Nam tortor risus, faucibus nec lacinia ac, laoreet vitae diam.</p>
<p>Nullam viverra elementum mi, at tincidunt nisl pretium a. Fusce risus risus, ornare facilisis leo a, dictum porta nibh.</p>
<p>Quisque ut augue tortor. Phasellus hendrerit placerat molestie.</p>
</div>
<div class="stats">
Assigned To: admin
<span class="right">
<span class="icon icon-chat-empty" title="4 Comments"></span>
<span class="icon icon-attach" title="2 Attachments"></span>
<span class="category" title="Category">Front End</span>
</span>
</div>
<div class="quick-add">
<input type="text" placeholder="Quick Add Task - Title Only">
<button class="flat"><i class="icon icon-plus"></i></button>
</div>
</div>
@ -90,23 +98,31 @@
<span class="right icon icon-angle-double-down"
title="Collapse Column"></span>
</h3>
<div class="task compact">
<h4>
<span class="icon icon-plus-squared-alt" title="Expand Task"></span>
Something Else to Get Done
<span class="badge right" title="Points">5</span>
</h4>
<div class="description">
<p>This really needs to be taken care of as well.</p>
<p>But probably not as quickly as the other one. Maybe.</p>
</div>
<div class="stats">
Assigned To: Unassigned
<span class="right">
<span class="category" title="Category">API</span>
</span>
<div class="tasks">
<div class="task compact">
<h4>
<span class="icon icon-plus-squared-alt" title="Expand Task"></span>
Something Else to Get Done
<span class="badge right" title="Points">5</span>
</h4>
<div class="description">
<p>This really needs to be taken care of as well.</p>
<p>But probably not as quickly as the other one. Maybe.</p>
</div>
<div class="stats">
Assigned To: Unassigned
<span class="right">
<span class="category" title="Category">API</span>
</span>
</div>
</div>
</div>
<div class="quick-add">
<input type="text" placeholder="Quick Add Task - Title Only">
<button class="flat"><i class="icon icon-plus"></i></button>
</div>
</div>
<div class="column">
@ -118,6 +134,11 @@
<span class="right icon icon-angle-double-down"
title="Collapse Column"></span>
</h3>
<div class="quick-add">
<input type="text" placeholder="Quick Add Task - Title Only">
<button class="flat"><i class="icon icon-plus"></i></button>
</div>
</div>
<div class="column collapsed">
@ -130,19 +151,27 @@
<span class="right icon icon-angle-double-down"
title="Collapse Column"></span>
</h3>
<div class="task">
<h4>Something Else to Get Done <span class="badge right" title="Points">5</span></h4>
<div class="description">
<p>This really needs to be taken care of as well.</p>
<p>But probably not as quickly as the other one. Maybe.</p>
</div>
<div class="stats">
Assigned To: Unassigned
<span class="right">
<span class="category" title="Category">API</span>
</span>
<div class="tasks">
<div class="task">
<h4>Something Else to Get Done <span class="badge right" title="Points">5</span></h4>
<div class="description">
<p>This really needs to be taken care of as well.</p>
<p>But probably not as quickly as the other one. Maybe.</p>
</div>
<div class="stats">
Assigned To: Unassigned
<span class="right">
<span class="category" title="Category">API</span>
</span>
</div>
</div>
</div>
<div class="quick-add">
<input type="text" placeholder="Quick Add Task - Title Only">
<button class="flat"><i class="icon icon-plus"></i></button>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { TopNav } from '../top-nav/top-nav.component';
import { TopNav } from '../shared/index';
@Component({
selector: 'tb-board',

View File

@ -1,3 +1,5 @@
<tb-top-nav page-name="Dashboard"></tb-top-nav>
<div class="dashboard">
<section>
<h2>Boards and Tasks</h2>

View File

@ -1,12 +1,13 @@
import { Component } from '@angular/core';
import { TopNav } from '../shared/index';
import { Charts } from './charts/charts.component';
import { Calendar } from './calendar/calendar.component';
@Component({
selector: 'tb-dashboard',
templateUrl: 'app/dashboard/dashboard.component.html',
directives: [ Charts, Calendar ]
directives: [ TopNav, Charts, Calendar ]
})
export class Dashboard {
}

View File

@ -1,9 +1,15 @@
<div class="login">
<h1>TaskBoard</h1>
<input type="text" placeholder="Username" title="Username" autofocus>
<input type="password" placeholder="Password" title="Password">
<label><input type="checkbox"> Remember Me</label>
<button>Sign In</button>
<form #f="ngForm">
<h1>TaskBoard</h1>
<input type="text" placeholder="Username" title="Username"
[(ngModel)]="username" name="username" autofocus>
<input type="password" placeholder="Password" title="Password"
[(ngModel)]="password" name="password">
<label><input type="checkbox"
[(ngModel)]="remember" name="remember"> Remember Me</label>
<button type="submit" [disabled]="isSubmitted"
(click)="login()" >Sign In</button>
</form>
<p>v{{ version }}</p>
</div>

View File

@ -1,6 +1,13 @@
import { Component } from "@angular/core";
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Constants } from '../app.constants';
import {
AuthService,
ApiResponse,
Notification,
NotificationsService
} from '../shared/index';
@Component({
selector: 'tb-login',
@ -8,9 +15,31 @@ import { Constants } from '../app.constants';
})
export class Login {
version: string;
username: string;
password: string;
remember: boolean;
isSubmitted: boolean = false;
constructor(constants: Constants) {
constructor(constants: Constants, private authService: AuthService,
private router: Router, private notes: NotificationsService) {
this.version = constants.VERSION;
}
login(): void {
this.isSubmitted = true;
this.authService.login(this.username, this.password, this.remember).
subscribe((response: ApiResponse) => {
response.alerts.forEach(msg => {
this.notes.add(new Notification(msg.type, msg.text));
});
if (response.status === 'success') {
this.router.navigate(['/boards']);
}
this.isSubmitted = false;
});
}
}

View File

@ -1,16 +1,23 @@
import { bootstrap } from '@angular/platform-browser-dynamic';
import { HTTP_PROVIDERS } from '@angular/http';
import { disableDeprecatedForms, provideForms } from '@angular/forms';
//import { enableProdMode } from '@angular/core';
import { AppComponent } from './app.component';
import { APP_ROUTER_PROVIDERS } from './app.routes';
import { Constants } from './app.constants';
import { NotificationsService } from './shared/index';
//enableProdMode();
bootstrap(AppComponent, [
HTTP_PROVIDERS,
APP_ROUTER_PROVIDERS,
Constants
]);
bootstrap(AppComponent,
[
HTTP_PROVIDERS,
disableDeprecatedForms(),
provideForms(),
APP_ROUTER_PROVIDERS,
Constants,
NotificationsService
]
);

View File

@ -1,3 +1,5 @@
<tb-top-nav page-name="Settings"></tb-top-nav>
<div class="settings">
<div class="half-page">
<section>
@ -33,7 +35,7 @@
</select>
</label>
<label>
Display two cards per column?
Display tasks side-by-side in colums?
<input type="checkbox">
</label>
</div>

View File

@ -1,8 +1,11 @@
import { Component } from '@angular/core';
import { TopNav } from '../shared/index';
@Component({
selector: 'tb-settings',
templateUrl: 'app/settings/settings.component.html'
templateUrl: 'app/settings/settings.component.html',
directives: [ TopNav ]
})
export class Settings {
}

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {
}
canActivate() {
if (this.authService.isLoggedIn) {
return true;
}
this.router.navigate(['']);
return false;
}
}

View File

@ -0,0 +1,57 @@
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import { User, ApiResponse } from '../index';
@Injectable()
export class AuthService {
activeUser: User;
isLoggedIn: boolean = false;
constructor(private http: Http) {
}
login(username: string, password: string,
remember: boolean): Observable<ApiResponse> {
// TODO Add remember flag to API
let json = JSON.stringify({
username: username,
password: password
});
return this.http.post('api/login', json).
map(res => {
let response: ApiResponse = res.json();
if (res.status === 200) {
this.isLoggedIn = true;
this.activeUser = response.data[1];
localStorage.setItem('jwt', response.data[0])
}
return response;
}).
catch((res, caught) => {
let response: ApiResponse = res.json();
if (res.status === 401) {
this.activeUser = null;
this.isLoggedIn = false;
localStorage.removeItem('jwt');
}
return Observable.of(response);
});
}
logout(): void {
this.isLoggedIn = false;
}
}

View File

@ -0,0 +1,3 @@
export * from './auth.guard';
export * from './auth.service';

5
src/app/shared/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './top-nav/top-nav.component';
export * from './auth/index';
export * from './models/index';
export * from './notifications/index';

View File

@ -0,0 +1,8 @@
import { Notification } from './notification.model';
export interface ApiResponse {
alerts: Array<Notification>;
data: Array<any>;
status: string;
}

View File

@ -0,0 +1,4 @@
export * from './user.model';
export * from './api-response.model';
export * from './notification.model';

View File

@ -0,0 +1,10 @@
export class Notification {
type: string;
text: string;
constructor(type: string, text: string) {
this.type = type;
this.text = text;
}
}

View File

@ -0,0 +1,10 @@
export interface User {
default_board_id: number;
email: string;
id: number;
last_login: Date;
security_level: number;
user_option_id: number;
username: string;
}

View File

@ -0,0 +1,3 @@
export * from './notifications.component';
export * from './notifications.service';

View File

@ -0,0 +1,7 @@
<div class="notifications">
<div (click)="hide(note)" class="{{ note.type }}"
*ngFor="let note of notes">
{{ note.text }}
</div>
</div>

View File

@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import { Notification } from '../models/notification.model';
import { NotificationsService } from './notifications.service';
@Component({
selector: 'tb-notifications',
templateUrl: 'app/shared/notifications/notifications.component.html'
})
export class Notifications {
private notes: Notification[];
constructor(private notifications: NotificationsService) {
this.notes = new Array<Notification>();
notifications.noteAdded.subscribe(note => {
this.notes.push(note);
setTimeout(() => { this.hide.bind(this)(note) }, 3000);
});
}
private hide(note: Notification): void {
let index = this.notes.indexOf(note);
note.type = note.type + " clicked";
if (index >= 0) {
setTimeout(() => {
this.notes.splice(index, 1);
}, 500);
}
}
}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Notification } from '../models/notification.model';
@Injectable()
export class NotificationsService {
private notifications = new Subject<Notification>();
public noteAdded = this.notifications.asObservable();
public add(notification: Notification): void {
console.log(notification);
this.notifications.next(notification);
}
}

View File

@ -0,0 +1,23 @@
<nav class="nav-top">
<h1>
TaskBoard
<span>
<small>
- {{ pageName }}
<small class="dark small">(v{{ version }})</small>
</small>
</span>
</h1>
<div class="buttons">
<button [ngClass]="{ flat: !isActive('dashboard') }"
(click)="navigateTo('dashboard')">Dashboard</button>
<button [ngClass]="{ flat: !isActive('boards') }"
(click)="navigateTo('boards')">Boards</button>
<button [ngClass]="{ flat: !isActive('settings') }"
(click)="navigateTo('settings')">Settings</button>
<button class="flat"
(click)="logout()">Logout (username)</button>
</div>
</nav>

View File

@ -0,0 +1,34 @@
import { Component, Input } from '@angular/core';
import { ROUTER_DIRECTIVES, Router } from '@angular/router';
import { Constants } from '../../app.constants';
import { AuthService } from '../auth/index';
@Component({
selector: 'tb-top-nav',
templateUrl: 'app/shared/top-nav/top-nav.component.html',
directives: [ ROUTER_DIRECTIVES ]
})
export class TopNav {
@Input('page-name') pageName: string;
version: string;
constructor(constants: Constants, private router: Router,
private authService: AuthService) {
this.version = constants.VERSION;
}
logout(): void {
this.authService.logout();
this.router.navigate(['']);
}
isActive(route: string): boolean {
return this.router.url.indexOf(route) !== -1;
}
navigateTo(target: string): void {
this.router.navigate(['/' + target]);
}
}

View File

@ -1,19 +0,0 @@
<nav class="nav-top">
<h1>
TaskBoard
<span>
<small>
- {{ pageName }}
<small class="dark small">(v{{ version }})</small>
</small>
</span>
</h1>
<div class="buttons">
<button class="flat">Dashboard</button>
<button>Boards</button>
<button class="flat">Settings</button>
<button class="flat">Logout (username)</button>
</div>
</nav>

View File

@ -1,18 +0,0 @@
import { Component } from '@angular/core';
import { Constants } from '../app.constants';
@Component({
selector: 'tb-top-nav',
templateUrl: 'app/top-nav/top-nav.component.html'
})
export class TopNav {
pageName: string;
version: string;
constructor(constants: Constants) {
this.pageName = 'TaskBoard Re-Write';
this.version = constants.VERSION;
}
}

View File

@ -45,10 +45,7 @@
cursor: pointer;
}
.icon-angle-double-up {
display: none;
}
.icon-angle-double-up,
.badge {
display: none;
}
@ -72,29 +69,51 @@
white-space: nowrap;
}
.icon-minus-squared-alt,
.icon-plus-squared-alt {
display: none;
}
.badge {
display: inline-block;
font-size: .6em;
margin-left: 1em;
margin-left: .9em;
transform: rotate(-90deg) translateX(4px);
}
.task {
.icon-angle-double-down,
.icon-minus-squared-alt,
.icon-plus-squared-alt,
.task,
.quick-add {
display: none;
}
.icon-angle-double-up {
display: inline-block;
}
}
}
.icon-angle-double-down {
display: none;
}
.quick-add {
padding: 7px;
input {
width: calc(100% - 42px);
}
button {
background-color: $white;
border-color: $color-border;
color: $color-text;
height: 36px;
margin-left: 2px;
padding: 9px;
vertical-align: bottom;
}
}
.tasks {
padding: 7px;
padding-bottom: 0;
div:last-of-type {
margin-bottom: 0;
}
}
@ -102,7 +121,7 @@
@include shadow-low();
background-color: #ffffe0;
margin: 5px;
margin-bottom: 7px;
h4 {
border-bottom: 1px solid lighten($color-border, 25%);

View File

@ -2,12 +2,18 @@
@include shadow-low();
background-color: $white;
margin: 3em auto;
display: inline-block;
margin: 3em 41%;
max-width: 350px;
padding: 1em;
position: relative;
text-align: center;
form {
box-shadow: none;
padding: 0;
}
h1 {
margin: 1em;
margin-top: .5em;

View File

@ -28,6 +28,10 @@
background-color: lighten($color-background, 10%);
color: $color-primary;
}
button {
cursor: pointer;
}
}
}

View File

@ -0,0 +1,54 @@
$success: #dff2bf;
$warn: #feefb3;
$error: #ffbaba;
$info: #bde5f8;
.notifications {
bottom: 1%;
left: calc(50% - 200px);
position: absolute;
div {
@include shadow-float;
border: 1px solid;
border-radius: 2px;
cursor: pointer;
margin-bottom: .5em;
min-height: 3em;
opacity: 1;
padding: 1em;
text-align: center;
transition: opacity .5s linear;
width: 400px;
&.clicked {
opacity: 0;
}
&.success {
background-color: $success;
border-color: darken($success, 30%);
color: darken($success, 60%);
}
&.error {
background-color: $error;
border-color: darken($error, 10%);
color: darken($error, 60%);
}
&.warn {
background-color: $warn;
border-color: darken($warn, 30%);
color: darken($warn, 57%);
}
&.info {
background-color: $info;
border-color: darken($info, 30%);
color: darken($info, 50%);
}
}
}

View File

@ -19,4 +19,5 @@
@import 'settings';
@import 'dashboard';
@import 'icons';
@import 'notifications';

View File

@ -14,6 +14,7 @@
'@angular/common',
'@angular/compiler',
'@angular/core',
'@angular/forms',
'@angular/http',
'@angular/platform-browser',
'@angular/platform-browser-dynamic',