├── .gitignore ├── LICENSE ├── README.md ├── projector-server ├── package.json └── server.js ├── projector ├── app │ ├── NewProject.component.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth.service.ts │ ├── conversation.component.ts │ ├── conversation.ts │ ├── emitter.service.ts │ ├── home.component.ts │ ├── loggedIn.guard.ts │ ├── login.component.ts │ ├── main.ts │ ├── newConversation.component.ts │ ├── pair.pipe.ts │ ├── project.component.ts │ ├── project.ts │ ├── projectSummary.component.ts │ ├── projects.component.ts │ └── projects.service.ts ├── bs-config.js ├── css │ └── styles.css ├── index.html ├── package.json ├── systemjs.config.js ├── templates │ ├── app.component.html │ ├── conversation.component.html │ ├── home.component.html │ ├── login.component.html │ ├── newConversation.component.html │ ├── newProject.component.html │ ├── project.component.html │ ├── projectSummary.component.html │ └── projects.component.html └── tsconfig.json └── quickstart ├── index.html ├── package.json ├── systemjs.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | typings 4 | 5 | build 6 | *.js.map 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tuts+ 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Modern Web Apps With Angular][published url] 2 | ## Instructor: [Andrew Burgess][instructor url] 3 | 4 | 5 | Angular is more than just the next version of popular AngularJS front-end framework. Angular takes all the best parts of AngularJS and improves them. It's now a powerful and feature-complete framework that you can use to build the best web apps. Built with TypeScript in mind, Angular takes advantage of futuristic language features such as decorators and interfaces, which make coding faster and easier. Angular is also a great platform for building cross-platform mobile apps. 6 | 7 | If you want to create modern web apps with Angular, this is the course for you. In this course, Andrew Burgess will show you how to code a complete web app with Angular, using the most current features and architectural patterns. Follow along with Andrew and build a full-featured project management app, with user login and validation and real-time chat. You'll get lessons on the Angular template language, structuring your app, routing, form validation, services, observables, and more. So let's get started! 8 | 9 | We've built comprehensive guides to help you learn JavaScript and Angular, whether you're just getting started or you want to explore more advanced topics. 10 | 11 | - [Learn JavaScript: The Complete Guide](https://code.tutsplus.com/series/learn-javascript-the-complete-guide--cms-1112) 12 | - [Learn Angular](https://code.tutsplus.com/series/learn-angular--cms-1135) 13 | 14 | 15 | ## Source Files Description 16 | 17 | 18 | This repository contains the source code for the completed course project. In it you will find three subfolders: 19 | 20 | - **projector-server**: a simple backend for the project 21 | - **projector**: the completed course project 22 | - **quickstart**: the starting point for following along with the course 23 | 24 | ------ 25 | 26 | These are source files for the Envato Tuts+ course: [Modern Web Apps With Angular][published url] 27 | 28 | Available on [Tuts+](https://tutsplus.com). Teaching skills to millions worldwide. 29 | 30 | [published url]: https://code.tutsplus.com/courses/modern-web-apps-with-angular-2 31 | [instructor url]: https://tutsplus.com/authors/andrew-burgess 32 | -------------------------------------------------------------------------------- /projector-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "projector-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "db.js", 6 | "dependencies": { 7 | "body-parser": "1.15.2", 8 | "express": "4.14.0", 9 | "fantasy-readers": "0.0.1", 10 | "lodash": "4.16.4", 11 | "lowdb": "0.13.1", 12 | "ramda": "0.22.1", 13 | "uuid": "2.0.3" 14 | }, 15 | "devDependencies": {}, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "start": "node server.js" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /projector-server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const app = express(); 4 | const router = express.Router(); 5 | 6 | var id = 1; 7 | const getId = () => '' + id++; 8 | var PROJECTS = []; 9 | var USERS = []; 10 | 11 | const makeUser = (attrs) => { 12 | var user = Object.assign({}, attrs, { id: getId() }); 13 | USERS.push(user); 14 | return user; 15 | }; 16 | 17 | const userBy = (prop, val) => USERS.filter(u => u[prop] === val)[0]; 18 | 19 | const makeProject = attrs => { 20 | var proj = Object.assign({}, attrs, { id: getId(), conversations: [] }); 21 | PROJECTS.push(proj); 22 | return proj; 23 | }; 24 | 25 | const projectById = id => PROJECTS.filter(p => p.id === id)[0]; 26 | 27 | const makeTask = (attrs, proj) => { 28 | if (typeof proj === 'string') proj = projectById(proj); 29 | var task = Object.assign({}, attrs, { id: getId(), messages: [] }); 30 | proj.conversations.push(task); 31 | return task; 32 | }; 33 | 34 | const taskById = id => { 35 | return PROJECTS.reduce((task, nextProj) => { 36 | return task || nextProj.conversations.filter(t => t.id === id)[0]; 37 | }, null); 38 | } 39 | 40 | const makeMessage = (attrs, task, user) => { 41 | if (typeof task === 'string') task = taskById(task); 42 | if (!user) user = userBy('username', attrs.username); 43 | 44 | var msg = Object.assign({}, attrs, { id: getId(), username: user.username, avatar: user.avatar }); 45 | task.messages.push(msg); 46 | return msg; 47 | }; 48 | 49 | const authenticateLogin = ({ username, password }) => { 50 | let user = USERS.filter(u => u.username === username)[0]; 51 | return (user && user.password === password) ? user : false; 52 | }; 53 | 54 | // INITIAL DATA 55 | var u1 = makeUser({ username: 'eddie', password: 'eddie', avatar: 'https://randomuser.me/api/portraits/men/91.jpg' }); 56 | var u2 = makeUser({ username: 'april', password: 'april', avatar: 'https://randomuser.me/api/portraits/women/31.jpg' }); 57 | var u3 = makeUser({ username: 'jimmy', password: 'jimmy', avatar: 'https://randomuser.me/api/portraits/men/67.jpg' }); 58 | var u3 = makeUser({ username: 'linn', password: 'linn', avatar: 'https://randomuser.me/api/portraits/women/47.jpg' }); 59 | 60 | var p1 = makeProject({ users: ['jimmy', 'eddie', 'april', 'linn'], name: 'Foobar App', description: 'Codename: The Killer App' }); 61 | var t1 = makeTask({ name: 'UI Design' }, p1); 62 | makeMessage({ text: 'How are the mockups coming?' }, t1, u1); 63 | makeMessage({ text: 'Looking pretty good! Linn has come up with some great stuff.' }, t1, u2); 64 | makeMessage({ text: 'Aw, thanks! :)' }, t1, u3); 65 | var t2 = makeTask({ name: 'Weekly Meeting Oct. 24' }, p1); 66 | 67 | var p2 = makeProject({ users: ['jimmy', 'eddie', 'april', 'linn'], name: 'Client XYZ Project', description: 'The one about the thing.' }); 68 | var t3 = makeTask({ name: 'Task B1' }, p2); 69 | var t4 = makeTask({ name: 'Task B2' }, p2); 70 | 71 | var p3 = makeProject({ users: ['eddie', 'april', 'linn'], name: 'Annual Company Dinner', description: 'Not McDonalds Again' }); 72 | var t3 = makeTask({ name: 'Task C1' }, p3); 73 | var t4 = makeTask({ name: 'Task C1' }, p3); 74 | 75 | router.route('/projects') 76 | .get((req, res) => res.status(200).json(PROJECTS)) 77 | .post((req, res) => res.status(200).json(makeProject(req.body))); 78 | 79 | router.route('/projects/:id') 80 | .get((req, res) => res.status(200).json(projectById(req.params.id))); 81 | 82 | router.route('/projects/:id/conversations') 83 | .post((req, res) => res.status(200).json(makeTask(req.body, req.params.id))); 84 | 85 | router.route('/conversations/:id') 86 | .get((req, res) => res.status(200).json(taskById(req.params.id))); 87 | 88 | router.route('/conversations/:id/messages') 89 | .post((req, res) => res.status(200).json(makeMessage(req.body, req.params.id))); 90 | 91 | router.route('/users') 92 | .get((req, res) => res.status(200).json(USERS)); 93 | 94 | router.route('/login') 95 | .post((req, res) => res.status(200).json(authenticateLogin(req.body))); 96 | 97 | app 98 | .use(bodyParser.json()) 99 | .use('/api', router).listen(9876); 100 | -------------------------------------------------------------------------------- /projector/app/NewProject.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AbstractControl, FormControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { AuthService } from './auth.service'; 6 | import { ProjectsService } from './projects.service'; 7 | 8 | @Component({ 9 | selector: 'new-project', 10 | templateUrl: 'templates/newProject.component.html', 11 | providers: [ProjectsService] 12 | }) 13 | export class NewProjectComponent implements OnInit { 14 | form: FormGroup; 15 | name: AbstractControl; 16 | submitted: boolean = false; 17 | users: any[]; 18 | 19 | constructor(private fb: FormBuilder, 20 | private router: Router, 21 | private auth: AuthService, 22 | private service: ProjectsService) {} 23 | 24 | ngOnInit() { 25 | this.form = this.fb.group({ 26 | name: ['', Validators.compose([ Validators.required, Validators.minLength(3) ])], 27 | description: '', 28 | users: new FormControl([]) 29 | }); 30 | 31 | this.name = this.form.controls['name']; 32 | 33 | this.service.getUsers().subscribe(users => { 34 | this.auth.currentUser.subscribe(currentUser => { 35 | let currUsername = currentUser.username; 36 | this.form.controls['users'].setValue([currUsername]); 37 | this.users = users.filter(u => u.username !== currUsername); 38 | }); 39 | }); 40 | } 41 | 42 | getIndex(username: string) : number { 43 | return this.form.controls['users'].value.indexOf(username); 44 | } 45 | 46 | isSelected(username: string) : boolean { 47 | return this.getIndex(username) > -1; 48 | } 49 | 50 | onSelected(evt, username) { 51 | let users = this.form.controls['users']; 52 | let newUsers = users.value; 53 | let index = this.getIndex(username); 54 | 55 | if (evt.checked && index === -1) { 56 | newUsers = users.value.concat([username]); 57 | } 58 | 59 | if (!evt.checked && index > -1) { 60 | newUsers = users.value.slice(0, index).concat(users.value.slice(index+1)); 61 | } 62 | 63 | users.setValue(newUsers); 64 | } 65 | 66 | handler() { 67 | if (this.form.valid) { 68 | this.service.createProject(this.form.value).subscribe(() => { 69 | this.router.navigateByUrl('/projects'); 70 | }); 71 | } else { 72 | this.submitted = true; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /projector/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'my-app', 7 | templateUrl: 'templates/app.component.html' 8 | }) 9 | export class AppComponent implements OnInit { 10 | user; 11 | 12 | constructor(private auth: AuthService, private router: Router) {} 13 | 14 | ngOnInit() { 15 | this.auth.currentUser.subscribe(user => this.user = user); 16 | } 17 | 18 | logout() { 19 | this.auth.logout(); 20 | this.router.navigateByUrl('/'); 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projector/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { MaterialModule } from '@angular/material'; 4 | import { RouterModule } from '@angular/router'; 5 | import { HttpModule } from '@angular/http'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | 8 | import { PairPipe } from './pair.pipe'; 9 | import { AuthService } from './auth.service'; 10 | import { AppComponent } from './app.component'; 11 | import { HomeComponent } from './home.component'; 12 | import { LoggedInGuard } from './loggedIn.guard'; 13 | import { EmitterService } from './emitter.service'; 14 | import { LoginComponent } from './login.component'; 15 | import { ProjectComponent, projectChildRoutes } from './project.component'; 16 | import { ProjectsComponent } from './projects.component'; 17 | import { NewProjectComponent } from './newProject.component'; 18 | import { ConversationComponent } from './conversation.component'; 19 | import { ProjectSummaryComponent } from './projectSummary.component'; 20 | import { NewConversationComponent } from './newConversation.component'; 21 | 22 | const routes = [ 23 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 24 | { path: 'home', component: HomeComponent }, 25 | { path: 'login', component: LoginComponent }, 26 | { path: 'projects', component: ProjectsComponent, canActivate: [ LoggedInGuard ] }, 27 | { path: 'projects/new', component: NewProjectComponent, canActivate: [ LoggedInGuard ] } 28 | { path: 'projects/:id', component: ProjectComponent, children: projectChildRoutes, canActivate: [ LoggedInGuard ] } 29 | ]; 30 | 31 | @NgModule({ 32 | imports: [ 33 | FormsModule, 34 | ReactiveFormsModule, 35 | HttpModule, 36 | BrowserModule, 37 | MaterialModule.forRoot(), 38 | RouterModule.forRoot(routes) 39 | ], 40 | declarations: [ 41 | PairPipe, 42 | AppComponent, 43 | HomeComponent, 44 | LoginComponent, 45 | ProjectComponent, 46 | ProjectsComponent, 47 | NewProjectComponent, 48 | ConversationComponent, 49 | ProjectSummaryComponent, 50 | NewConversationComponent 51 | ], 52 | bootstrap: [ AppComponent ], 53 | providers: [ AuthService, LoggedInGuard, EmitterService ] 54 | }) 55 | export class AppModule {} 56 | -------------------------------------------------------------------------------- /projector/app/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http } from '@angular/http'; 3 | import { Observable, BehaviorSubject } from 'rxjs/Rx'; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | currentUser: BehaviorSubject = new BehaviorSubject(false); 8 | 9 | constructor(private http: Http) { 10 | this.currentUser.next(this.getUser()); 11 | } 12 | 13 | login(username: string, password: string) : Observable { 14 | return this.http.post('/api/login', { username, password }) 15 | .map(res => res.json()) 16 | .map(user => { 17 | if (user) { 18 | localStorage.setItem('user', JSON.stringify(user)); 19 | this.currentUser.next(user); 20 | } 21 | return !!user; 22 | }); 23 | } 24 | 25 | logout() { 26 | localStorage.removeItem('user'); 27 | this.currentUser.next(false); 28 | } 29 | 30 | getUser() { 31 | var user = localStorage.getItem('user'); 32 | return user ? JSON.parse(user) : false; 33 | } 34 | 35 | isLoggedIn(): boolean { 36 | return !!this.getUser(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projector/app/conversation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestory } from '@angular/core'; 2 | import { ActivatedRoute, Params, Router } from '@angular/router'; 3 | import { Observable, Subscription } from 'rxjs/Rx'; 4 | 5 | import { ProjectsService } from './projects.service'; 6 | import { AuthService } from './auth.service'; 7 | import { Conversation } from './conversation'; 8 | import { Project } from './project'; 9 | 10 | @Component({ 11 | selector: 'conversation', 12 | templateUrl: 'templates/conversation.component.html', 13 | providers: [ ProjectsService ] 14 | }) 15 | export class ConversationComponent implements OnInit, OnDestroy { 16 | conv: Conversation; 17 | project: Observable; 18 | writable: boolean = false; 19 | message: string; 20 | sub: Subscription; 21 | 22 | constructor(private auth: AuthService, 23 | private service: ProjectsService, 24 | private router: Router, 25 | private route: ActivatedRoute) { 26 | this.conv_id = route.params.map((params: Params) => params['conv_id']) 27 | this.username = auth.currentUser.map(u => u.username); 28 | 29 | this.project = router 30 | .routerState 31 | .parent(route) 32 | .params 33 | .map((params: Params) => params['id']) 34 | .flatMap(id => this.service.getProject(id)); 35 | } 36 | 37 | ngOnInit() { 38 | this.username.subscribe(username => { 39 | this.project.subscribe(project => { 40 | this.writable = project.users.indexOf(username) > -1; 41 | }); 42 | }); 43 | 44 | this.sub = Observable.timer(0, 5000) 45 | .flatMap(() => this.conv_id.flatMap(id => this.service.getConversation(id))) 46 | .subscribe((conversation: Conversation) => { 47 | this.conv = conversation; 48 | }); 49 | } 50 | 51 | handleClick() { 52 | this.username.subscribe(username => { 53 | let msg = { text: this.message, username }; 54 | 55 | this.conv_id 56 | .flatMap(id => this.service.createMessage(id, msg)) 57 | .subscribe(message => { 58 | this.conv.messages.push(message); 59 | this.message = ''; 60 | }); 61 | }); 62 | } 63 | 64 | ngOnDestroy() { 65 | this.sub.unsubscribe(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /projector/app/conversation.ts: -------------------------------------------------------------------------------- 1 | export class Conversation { 2 | id: string; 3 | name: string; 4 | messages: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /projector/app/emitter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs/Rx'; 3 | 4 | @Injectable() 5 | export class EmitterService { 6 | private events = new Subject(); 7 | 8 | subscribe(next, error, onComplete) { 9 | return this.events.subscribe(next, error, onComplete); 10 | } 11 | 12 | next(evt) { 13 | this.events.next(evt); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projector/app/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | 4 | @Component({ 5 | selector: 'home', 6 | templateUrl: 'templates/home.component.html' 7 | }) 8 | export class HomeComponent implements OnInit { 9 | loggedIn: boolean = false; 10 | 11 | constructor(private auth: AuthService) {} 12 | 13 | ngOnInit() { 14 | this.auth.currentUser.subscribe(user => { 15 | this.loggedIn = !!user; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projector/app/loggedIn.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Injectable() 6 | export class LoggedInGuard implements CanActivate { 7 | constructor (private auth: AuthService) {} 8 | 9 | canActivate(): boolean { 10 | return this.auth.isLoggedIn(); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /projector/app/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Component({ 6 | selector: 'login', 7 | templateUrl: 'templates/login.component.html' 8 | }) 9 | export class LoginComponent { 10 | constructor (private auth: AuthService, private router: Router) {} 11 | 12 | login({ username, password }) { 13 | this.auth.login(username, password).subscribe(loggedIn => { 14 | if (loggedIn) { 15 | this.router.navigateByUrl('/home'); 16 | } 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projector/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app.module'; 3 | 4 | const platform = platformBrowserDynamic(); 5 | platform.bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /projector/app/newConversation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs/Rx'; 3 | import { Router, ActivatedRoute } from '@angular/router'; 4 | import { ProjectsService } from './projects.service'; 5 | import { EmitterService } from './emitter.service'; 6 | 7 | @Component({ 8 | selector: 'new-conversation', 9 | templateUrl: 'templates/newConversation.component.html', 10 | providers: [ ProjectsService] 11 | }) 12 | export class NewConversationComponent { 13 | name: string; 14 | project_id: Observable; 15 | 16 | constructor(private router: Router, 17 | private route: ActivatedRoute, 18 | private emitter: EmitterService, 19 | private service: ProjectsService) { 20 | this.project_id = router 21 | .routerState 22 | .parent(route) 23 | .params 24 | .map(params => params['id']); 25 | } 26 | 27 | handleClick() { 28 | this.project_id 29 | .flatMap(id => this.service.createConversation(id, this.name)) 30 | .subscribe(conv => { 31 | this.emitter.next(conv); 32 | this.router.navigate(['../', conv.id], { relativeTo: this.route }) 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projector/app/pair.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'pair' 5 | }) 6 | export class PairPipe implements PipeTransform { 7 | transform(value: any[]): any[] { 8 | if (!value) return null; 9 | 10 | let arr = []; 11 | let i = 0; 12 | 13 | while(value[i]) { 14 | let item = []; 15 | 16 | item.push(value[i++]); 17 | if (value[i]) 18 | item.push(value[i++]); 19 | 20 | arr.push(item); 21 | } 22 | return arr; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projector/app/project.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute, Params } from '@angular/router'; 3 | 4 | import { EmitterService } from './emitter.service'; 5 | import { ProjectsService } from './projects.service'; 6 | import { AuthService } from './auth.service'; 7 | import { Project } from './project'; 8 | 9 | import { ConversationComponent } from './conversation.component'; 10 | import { NewConversationComponent } from './newConversation.component'; 11 | 12 | export const projectChildRoutes = [ 13 | { path: '', component: ProjectComponent }, 14 | { path: 'conversations/new', component: NewConversationComponent }, 15 | { path: 'conversations/:conv_id', component: ConversationComponent } 16 | ]; 17 | 18 | @Component({ 19 | selector: 'project', 20 | templateUrl: 'templates/project.component.html', 21 | providers: [ProjectsService] 22 | }) 23 | export class ProjectComponent implements OnInit, OnDestroy { 24 | project: Project; 25 | canWork: boolean = false; 26 | 27 | constructor(private auth: AuthService, 28 | private route: ActivatedRoute, 29 | private emitter: EmitterService, 30 | private service: ProjectsService) {} 31 | 32 | ngOnInit() { 33 | this.route.params 34 | .map((params: Params) => params['id']) 35 | .flatMap(id => this.service.getProject(id)) 36 | .subscribe((project: Project) => { 37 | this.project = project; 38 | 39 | this.sub = this.emitter.subscribe(conv => this.project.conversations.push(conv)); 40 | 41 | this.auth.currentUser.subscribe(user => { 42 | this.canWork = project.users.indexOf(user.username) > -1; 43 | }); 44 | }); 45 | } 46 | 47 | ngOnDestroy () { 48 | this.sub.unsubscribe(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /projector/app/project.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from './conversation'; 2 | 3 | export class Project { 4 | id: string; 5 | name: string; 6 | description: string; 7 | conversations: Conversation[] 8 | users: any[] 9 | } 10 | -------------------------------------------------------------------------------- /projector/app/projectSummary.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { Project } from './project'; 4 | import { ProjectsService } from './projects.service'; 5 | 6 | @Component({ 7 | selector: 'project-summary', 8 | templateUrl: 'templates/projectSummary.component.html', 9 | providers: [ProjectsService] 10 | }) 11 | export class ProjectSummaryComponent implements OnInit { 12 | @Input() project: Project; 13 | avatars: string[]; 14 | 15 | constructor(private service: ProjectsService) {} 16 | 17 | ngOnInit() { 18 | this.service.getUsers().subscribe(users => { 19 | this.avatars = users 20 | .filter(u => this.project.users.indexOf(u.username) > -1) 21 | .map(u => u.avatar); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projector/app/projects.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ProjectsService } from './projects.service'; 4 | import { Project } from './project'; 5 | 6 | @Component({ 7 | selector: 'projects', 8 | templateUrl: 'templates/projects.component.html', 9 | providers: [ProjectsService] 10 | }) 11 | export class ProjectsComponent implements OnInit { 12 | projects: Project[]; 13 | 14 | constructor(private service: ProjectsService) {} 15 | 16 | ngOnInit() { 17 | this.service.getProjects().subscribe(projects => 18 | this.projects = projects); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /projector/app/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Project } from './project'; 5 | import { Conversation } from './conversation'; 6 | 7 | @Injectable() 8 | export class ProjectsService { 9 | constructor(private http: Http) {} 10 | 11 | extractData(res: Response) { 12 | return res.json(); 13 | } 14 | 15 | getUsers(): Observable { 16 | return this.http.get('/api/users').map(this.extractData); 17 | } 18 | 19 | getProject(id: string) : Observable { 20 | return this.http.get('/api/projects/' + id).map(this.extractData); 21 | } 22 | 23 | getProjects(): Observable { 24 | return this.http.get('/api/projects').map(this.extractData); 25 | } 26 | 27 | createProject(attrs): Observable { 28 | return this.http.post('/api/projects', attrs).map(this.extractData); 29 | } 30 | 31 | getConversation(id: string) : Observable { 32 | return this.http.get('/api/conversations/' + id).map(this.extractData); 33 | } 34 | 35 | createConversation(id: string, name: string) : Observable { 36 | return this.http.post(`/api/projects/${id}/conversations`, { name }).map(this.extractData); 37 | } 38 | 39 | createMessage(conv_id: string, message: string) { 40 | return this.http.post('/api/conversations/' + conv_id + '/messages', message).map(this.extractData); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projector/bs-config.js: -------------------------------------------------------------------------------- 1 | const proxyMiddleware = require('http-proxy-middleware'); 2 | const proxyURL = 'http://localhost:9876'; 3 | 4 | module.exports = { 5 | ghostMode: false, 6 | server: { 7 | middleware: [proxyMiddleware('/api', { target: proxyURL })] 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /projector/css/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Roboto, "Helvetica Neue", sans-serif; 5 | } 6 | 7 | .container { 8 | width: 960px; 9 | margin: auto; 10 | } 11 | 12 | md-toolbar .md-tool { 13 | display: inline-block; 14 | margin-right: 30px; 15 | } 16 | 17 | md-toolbar img { 18 | width: 40px; 19 | height: 40px; 20 | border-radius:50%; 21 | margin-right:10px; 22 | } 23 | 24 | md-icon { 25 | color: #757575; 26 | } 27 | 28 | .project-pair project-summary { 29 | display: inline-block; 30 | width: 49%; 31 | float: left; 32 | } 33 | 34 | .project-pair project-summary:last-child { 35 | margin: 1% 0 1% 1%; 36 | } 37 | 38 | .project-pair project-summary:first-child { 39 | margin: 1% 1% 1% 0; 40 | } 41 | 42 | .new-project md-input, .new-project textarea { 43 | width: 100%; 44 | } 45 | .new-project textarea { 46 | border: 1px solid rgba(0, 0, 0, 0.38); 47 | height: 50px; 48 | } 49 | 50 | .conv { 51 | padding: 1rem; 52 | margin: 1rem 0; 53 | background: #fff; 54 | border-radius: 5px; 55 | text-decoration: none; 56 | display: block; 57 | } 58 | 59 | .conv-full { 60 | _border: 1px solid #ccc; 61 | _background: #fff; 62 | box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15), 0px 0px 0px 1000px rgba(0,0,0,0.2); 63 | _border-radius: 5px; 64 | position: absolute; 65 | width: 50%; 66 | left: 25%; 67 | top: 100px; 68 | } 69 | 70 | .conv-full:hover { 71 | box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15), 0px 0px 0px 1000px rgba(0,0,0,0.2); 72 | } 73 | 74 | .full-width { 75 | width: 100%; 76 | } 77 | 78 | .avatar { 79 | margin-right: 10px; 80 | box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); 81 | } 82 | 83 | .error { 84 | padding: 5px; 85 | border: 1px solid red; 86 | color: red; 87 | background: rgba(255,0,0,0.25); 88 | } 89 | -------------------------------------------------------------------------------- /projector/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular QuickStart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | Loading... 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /projector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-quickstart", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ", 6 | "lite": "lite-server", 7 | "tsc": "tsc", 8 | "tsc:w": "tsc -w" 9 | }, 10 | "licenses": [ 11 | { 12 | "type": "MIT", 13 | "url": "https://github.com/angular/angular.io/blob/master/LICENSE" 14 | } 15 | ], 16 | "dependencies": { 17 | "@angular/common": "2.1.1", 18 | "@angular/compiler": "2.1.1", 19 | "@angular/core": "2.1.1", 20 | "@angular/forms": "2.1.1", 21 | "@angular/http": "2.1.1", 22 | "@angular/material": "latest", 23 | "@angular/platform-browser": "2.1.1", 24 | "@angular/platform-browser-dynamic": "2.1.1", 25 | "@angular/router": "3.1.1", 26 | "@angular/upgrade": "2.1.1", 27 | "angular-in-memory-web-api": "0.1.13", 28 | "core-js": "2.4.1", 29 | "http-proxy-middleware": "0.17.2", 30 | "reflect-metadata": "0.1.8", 31 | "rxjs": "5.0.0-beta.12", 32 | "systemjs": "0.19.39", 33 | "zone.js": "0.6.25" 34 | }, 35 | "devDependencies": { 36 | "@types/core-js": "0.9.34", 37 | "@types/node": "6.0.45", 38 | "concurrently": "3.0.0", 39 | "lite-server": "2.2.2", 40 | "typescript": "2.0.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projector/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': 'node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the build folder 14 | app: 'build', 15 | // angular bundles 16 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 17 | '@angular/material': 'npm:@angular/material/material.umd.js', 18 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 19 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 20 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 21 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 22 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 23 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 24 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 25 | '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js', 26 | // other libraries 27 | 'rxjs': 'npm:rxjs', 28 | 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js' 29 | }, 30 | // packages tells the System loader how to load when no filename and/or no extension 31 | packages: { 32 | app: { 33 | main: './main.js', 34 | defaultExtension: 'js' 35 | }, 36 | rxjs: { 37 | defaultExtension: 'js' 38 | } 39 | } 40 | }); 41 | })(this); 42 | 43 | -------------------------------------------------------------------------------- /projector/templates/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | Projector! 3 | Home 4 | Projects 5 | 6 | 7 | 8 | 9 | {{user.username}} | 10 | Log Out 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /projector/templates/conversation.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{conv.name}} 3 | 4 | 5 | 6 | 7 |

{{message.username}}

8 |

{{message.text}}

9 |
10 |
11 | 12 |

13 | 14 |

15 |

16 | 19 | 20 | 23 |

24 |
25 |
26 | -------------------------------------------------------------------------------- /projector/templates/home.component.html: -------------------------------------------------------------------------------- 1 |

Welcome!

2 | 3 |

4 | View your projects. 5 |

6 | 7 |

8 | Go to login form. 9 |

10 | -------------------------------------------------------------------------------- /projector/templates/login.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Login 5 | 6 | 7 |

8 | 9 |

10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /projector/templates/newConversation.component.html: -------------------------------------------------------------------------------- 1 | 2 | New Conversation 3 | 4 |

5 | 6 |

7 | 8 |

9 | 12 | 13 | 16 |

17 |
18 |
19 | -------------------------------------------------------------------------------- /projector/templates/newProject.component.html: -------------------------------------------------------------------------------- 1 | 2 | New Project 3 | 4 |
5 |

6 | 7 |

8 | 9 |

10 | 11 |

12 | 13 |
14 | 15 |
    16 |
  • 17 | 24 |
  • 25 |
26 |
27 | 28 | 31 | 32 |

33 | Name field is required. 34 |

35 | 36 |

37 | Name field must be longer than 2 characters. 38 |

39 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /projector/templates/project.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 8 | 9 | {{project.name}} 10 | {{project.description}} 11 | 12 | 15 | {{conv.name}} 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /projector/templates/projectSummary.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{project.name}} 4 | 5 | 6 | {{project.description}} 7 | 8 | 9 |

{{project.conversations.length}} Conversation(s)

10 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /projector/templates/projects.component.html: -------------------------------------------------------------------------------- 1 | 2 | New Project 3 | 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /projector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "removeComments": false, 11 | "noImplicitAny": false 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /quickstart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular QuickStart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | Loading... 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /quickstart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-quickstart", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ", 6 | "lite": "lite-server", 7 | "tsc": "tsc", 8 | "tsc:w": "tsc -w" 9 | }, 10 | "licenses": [ 11 | { 12 | "type": "MIT", 13 | "url": "https://github.com/angular/angular.io/blob/master/LICENSE" 14 | } 15 | ], 16 | "dependencies": { 17 | "@angular/common": "2.1.1", 18 | "@angular/compiler": "2.1.1", 19 | "@angular/core": "2.1.1", 20 | "@angular/forms": "2.1.1", 21 | "@angular/http": "2.1.1", 22 | "@angular/platform-browser": "2.1.1", 23 | "@angular/platform-browser-dynamic": "2.1.1", 24 | "@angular/router": "3.1.1", 25 | "@angular/upgrade": "2.1.1", 26 | "angular-in-memory-web-api": "0.1.13", 27 | "core-js": "2.4.1", 28 | "http-proxy-middleware": "0.17.2", 29 | "reflect-metadata": "0.1.8", 30 | "rxjs": "5.0.0-beta.12", 31 | "systemjs": "0.19.39", 32 | "zone.js": "0.6.25" 33 | }, 34 | "devDependencies": { 35 | "@types/core-js": "0.9.34", 36 | "@types/node": "6.0.45", 37 | "concurrently": "3.0.0", 38 | "lite-server": "2.2.2", 39 | "typescript": "2.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /quickstart/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': 'node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the build folder 14 | app: 'build', 15 | // angular bundles 16 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 17 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 18 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 19 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 20 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 21 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 22 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 23 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 24 | '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js', 25 | // other libraries 26 | 'rxjs': 'npm:rxjs', 27 | 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js' 28 | }, 29 | // packages tells the System loader how to load when no filename and/or no extension 30 | packages: { 31 | app: { 32 | main: './main.js', 33 | defaultExtension: 'js' 34 | }, 35 | rxjs: { 36 | defaultExtension: 'js' 37 | } 38 | } 39 | }); 40 | })(this); 41 | 42 | -------------------------------------------------------------------------------- /quickstart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "removeComments": false, 11 | "noImplicitAny": false 12 | } 13 | } 14 | 15 | --------------------------------------------------------------------------------