feat(aio): implement resources with resources.json

This commit is contained in:
Ward Bell 2017-04-14 17:53:49 -07:00 committed by Pete Bacon Darwin
parent 46b0c7a18c
commit 196203f6d7
21 changed files with 1445 additions and 29 deletions

View File

@ -0,0 +1,2 @@
<h1>Explore Angular Resources</h1>
<aio-resource-list></aio-resource-list>

View File

@ -0,0 +1,582 @@
{
"Community": {
"order": 3,
"subCategories": {
"Community Curations": {
"order": 1,
"resources": {
"awesome-angular-components": {
"desc": "A community index of components and libraries maintained on GitHub",
"rev": true,
"title": "Catalog of Angular Components & Libraries",
"url": "https://github.com/brillout/awesome-angular-components"
}
}
},
"Groups": {
"order": 2,
"resources": {
"sldkjfslkjfslkjdsklj": {
"desc": "Meetup in Barcelona, Spain. Express your motivations, share your ideas and play together creating awesome things in team.",
"rev": true,
"title": "Angular Beers",
"url": "http://www.meetup.com/AngularJS-Beers/"
},
"sldkjfslkjfslkjdskzzzlj": {
"desc": "Angular Conferences and Angular Camps in Barcelona, Spain.",
"rev": true,
"title": "Angular Camp",
"url": "http://angularcamp.org/"
}
}
},
"Podcasts": {
"order": 3,
"resources": {
"sdfjkdkfj": {
"desc": "Adventures in Angular is a weekly podcast dedicated to the Angular platform and related technologies, tools, languages, and practices.",
"logo": "",
"rev": true,
"title": "Adventures in Angular",
"url": "https://devchat.tv/adventures-in-angular"
},
"sdlkfjsldfkj": {
"desc": "Weekly video podcast hosted by Jeff Whelpley with all the latest and greatest happenings in the wild world of Angular.",
"logo": "",
"rev": true,
"title": "AngularAir",
"url": "https://angularair.com/"
},
"sldkfjsldjf": {
"desc": "The live broadcast podcast all about JavaScript",
"logo": "",
"rev": true,
"title": "Javascript Air",
"url": "https://javascriptair.com/"
}
}
}
}
},
"Development": {
"order": 1,
"subCategories": {
"Cross-Platform Development": {
"order": 5,
"resources": {
"a2b": {
"desc": "Angular 2 and React Native to build applications for Android and iOS",
"logo": "",
"rev": true,
"title": "ReactNative",
"url": "http://angular.github.io/react-native-renderer/"
},
"a3b": {
"desc": "Ionic offers a library of mobile-optimized HTML, CSS and JS components and tools for building highly interactive native and progressive web apps.",
"logo": "http://ionicframework.com/img/ionic-logo-white.svg",
"rev": true,
"title": "Ionic",
"url": "http://ionicframework.com/docs/v2/"
},
"a4b": {
"desc": "Electron Platform for Angular 2.",
"logo": "",
"rev": true,
"title": "Electron",
"url": "http://github.com/angular/angular-electron"
},
"ab": {
"desc": "NativeScript is how you build cross-platform, native iOS and Android apps with Angular and TypeScript. Get 100% access to native APIs via JavaScript and reuse of packages from NPM, CocoaPods and Gradle. Open source and backed by Telerik.",
"logo": "",
"rev": true,
"title": "NativeScript",
"url": "https://github.com/NativeScript/nativescript-angular"
},
"ab5": {
"desc": "An Universal Windows App (uwp) powered by Angular 2",
"logo": "",
"rev": true,
"title": "Windows (UWP)",
"url": "http://github.com/preboot/angular2-universal-windows-app"
}
}
},
"Data Libraries": {
"order": 3,
"resources": {
"-KLIzHDRfiB3d7W7vk-e": {
"desc": "Reactive Extensions for angular2",
"rev": true,
"title": "ngrx",
"url": "http://github.com/ngrx"
},
"ab": {
"desc": "The official library for Firebase and Angular 2",
"logo": "",
"rev": true,
"title": "Angular Fire",
"url": "https://github.com/angular/angularfire2"
},
"ab2": {
"desc": "Use Angular 2 and Meteor to build full-stack JavaScript apps for Mobile and Desktop.",
"logo": "http://www.angular-meteor.com/images/logo.png",
"rev": true,
"title": "Meteor",
"url": "http://www.angular-meteor.com/angular2"
},
"ab3": {
"desc": "Apollo is a data stack for modern apps, built with GraphQL.",
"logo": "http://docs.apollostack.com/logo/large.png",
"rev": true,
"title": "Apollo",
"url": "http://docs.apollostack.com/apollo-client/angular2.html"
}
}
},
"IDEs": {
"order": 1,
"resources": {
"ab": {
"desc": "VS Code is a Free, Lightweight Tool for Editing and Debugging Web Apps.",
"logo": "",
"rev": true,
"title": "Visual Studio Code",
"url": "http://code.visualstudio.com/"
},
"ab2": {
"desc": "Lightweight yet powerful IDE, perfectly equipped for complex client-side development and server-side development with Node.js",
"logo": "",
"rev": true,
"title": "Webstorm",
"url": "https://www.jetbrains.com/webstorm/"
},
"ab3": {
"desc": "Capable and Ergonomic Java * IDE",
"logo": "",
"rev": true,
"title": "IntelliJ IDEA",
"url": "https://www.jetbrains.com/idea/"
},
"angular-ide": {
"desc": "Built first and foremost for Angular. Turnkey setup for beginners; powerful for experts.",
"rev": true,
"title": "Angular IDE by Webclipse",
"url": "https://www.genuitec.com/products/angular-ide"
}
}
},
"Tooling": {
"order": 2,
"resources": {
"-KLIzHfBEr1qMMUDxfq3": {
"desc": "Generate an Angular 2 CRUD application from an existing database schema",
"rev": true,
"title": "Celerio Angular Quickstart",
"url": "https://github.com/jaxio/celerio-angular-quickstart"
},
"a1": {
"desc": "A Google Chrome Dev Tools extension for debugging Angular 2 applications.",
"logo": "https://augury.angular.io/images/augury-logo.svg",
"rev": true,
"title": "Augury",
"url": "http://augury.angular.io/"
},
"b1": {
"desc": "Server-side Rendering for Angular 2 apps.",
"logo": "https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png",
"rev": true,
"title": "Angular Universal",
"url": "https://github.com/angular/universal"
},
"c1": {
"desc": "Lightweight development only node server",
"logo": "",
"rev": true,
"title": "Lite-server",
"url": "https://github.com/johnpapa/lite-server"
},
"cli": {
"desc": "The official Angular CLI makes it easy to create and develop applications from initial commit to production deployment. It already follows our best practices right out of the box!",
"rev": true,
"title": "Angular CLI",
"url": "https://cli.angular.io"
},
"d1": {
"desc": "A set of tslint rules for static code analysis of Angular 2 TypeScript projects.",
"logo": "",
"rev": true,
"title": "Codelyzer",
"url": "https://github.com/mgechev/codelyzer"
},
"e1": {
"desc": "This package provides facilities for developers building Angular 2 applications on ASP.NET.",
"logo": "",
"rev": true,
"title": "Universal for ASP.NET",
"url": "https://github.com/aspnet/nodeservices"
}
}
},
"UI Components": {
"order": 4,
"resources": {
"234237": {
"desc": "UX guidelines, HTML/CSS framework, and Angular 2 components working together to craft exceptional experiences",
"rev": true,
"title": "Clarity Design System",
"url": "https://vmware.github.io/clarity/"
},
"-KLIzI9BTvvP_hUwutXk": {
"desc": "UI components for Angular using Semantic UI",
"rev": true,
"title": "Semantic UI",
"url": "https://github.com/vladotesanovic/ngSemantic"
},
"-KMVB8P4TDfht8c0L1AE": {
"desc": "The Angular 2 version of the Angular UI Bootstrap library. This library is being built from scratch in Typescript using the Bootstrap 4 CSS framework.",
"rev": true,
"title": "ng-bootstrap",
"url": "https://ng-bootstrap.github.io/"
},
"4ab": {
"desc": "Native Angular 2 components & directives for Lightning Design System",
"logo": "http://ng-lightning.github.io/ng-lightning/img/shield.svg",
"rev": true,
"title": "ng-lightning",
"url": "http://ng-lightning.github.io/ng-lightning/"
},
"7ab": {
"desc": "UI components for hybrid mobile apps with bindings for both Angular 1 & 2.",
"rev": true,
"title": "Onsen UI",
"url": "https://onsen.io/v2/"
},
"a2b": {
"desc": "PrimeNG is a collection of rich UI components for Angular 2",
"logo": "http://www.primefaces.org/primeng/showcase/resources/images/primeng.svg",
"rev": true,
"title": "Prime Faces",
"url": "http://www.primefaces.org/primeng/"
},
"a3b": {
"desc": "One of the first major UI frameworks to support Angular 2",
"logo": "",
"rev": true,
"title": "Kendo UI",
"url": "http://www.telerik.com/kendo-angular-ui/"
},
"a5b": {
"desc": "High-performance UI controls with the most complete Angular 2 support available. Wijmos controls are all written in TypeScript and have zero dependencies. FlexGrid control includes full declarative markup, including cell templates.",
"logo": "http://wijmocdn.azureedge.net/wijmositeblob/wijmo-theme/logos/wijmo-55.png",
"rev": true,
"title": "Wijmo",
"url": "http://wijmo.com/products/wijmo-5/"
},
"a6b": {
"desc": "Material design inspired UI components for building great web apps. For mobile and desktop.",
"logo": "",
"rev": true,
"title": "Vaadin",
"url": "https://vaadin.com/elements"
},
"a7b": {
"desc": "Native Angular2 directives for Bootstrap",
"logo": "",
"rev": true,
"title": "ng2-bootstrap",
"url": "http://valor-software.com/ng2-bootstrap/"
},
"ab": {
"desc": "Material Design components for Angular 2",
"logo": "",
"rev": true,
"title": "Angular Material 2",
"url": "https://github.com/angular/material2"
},
"aggrid": {
"desc": "A datagrid for Angular 2 with enterprise style features such as sorting, filtering, custom rendering, editing, grouping, aggregation and pivoting.",
"rev": true,
"title": "ag-Grid",
"url": "https://www.ag-grid.com/best-angular-2-data-grid/"
}
}
}
}
},
"Education": {
"order": 2,
"subCategories": {
"Books": {
"order": 1,
"resources": {
"-KLI8vJ0ZkvWhqPembZ7": {
"desc": "A guide that helps developers get up to speed quickly on Angular and its accompanying technologies.",
"rev": true,
"title": "How to Get Started and Productive in Angular 2 Fast",
"url": "http://www.amazon.com/How-Started-Productive-Angular-Fast-ebook/dp/B01D3B0ET4/ref=sr_1_1_twi_kin_2?ie=UTF8&qid=1462381159&sr=8-1"
},
"-KLIzGEp8Mh5W-FkiQnL": {
"desc": "Your quick, no-nonsense guide to building real-world apps with Angular",
"rev": true,
"title": "Learning Angular 2",
"url": "https://www.packtpub.com/web-development/learning-angular-2"
},
"3ab": {
"desc": "More than 15 books from O'Reilly about Angular",
"rev": true,
"title": "O'Reilly Media",
"url": "https://ssearch.oreilly.com/?q=angular+2&x=0&y=0"
},
"8ab": {
"desc": "This books shows all the steps necessary for the development of SPA (Single Page Application) applications with the brand new Angular",
"rev": true,
"title": "Practical Angular 2",
"url": "https://leanpub.com/practical-angular-2"
},
"a2b": {
"desc": "Publications and books from Manning about Angular",
"rev": true,
"title": "Manning Publications",
"url": "https://www.manning.com/search?q=angular"
},
"a4b": {
"desc": "From getting started with the Angular toolchain to writing applications with scalable front end architectures, this book walks you through everything you need to know.",
"rev": true,
"title": "Rangle's Angular Training Book",
"url": "http://ngcourse.rangle.io/"
},
"a5b": {
"desc": "The in-depth, complete, and up-to-date book on Angular. Become an Angular expert today.",
"rev": true,
"title": "ng-book 2",
"url": "https://www.ng-book.com/2/"
},
"a6b": {
"desc": "A Practical Introduction to the new Web Development Platform Angular",
"rev": true,
"title": "Angular 2 Book",
"url": "https://leanpub.com/angular2-book"
},
"a7b": {
"desc": "This ebook will help you getting the philosophy of the framework: what comes from 1.x, what has been introduced and why",
"rev": true,
"title": "Becoming a Ninja with Angular",
"url": "https://books.ninja-squad.com/angular"
},
"ab": {
"desc": "More than 10 books from Packt Publishing about Angular",
"rev": true,
"title": "Packt Publishing",
"url": "https://www.packtpub.com/all/?search=angular%202#"
},
"cnoring-rxjs-fundamentals": {
"desc": "A free book that covers all facets of working with Rxjs from your first Observable to how to make your code run at optimal speed with Schedulers.",
"rev": true,
"title": "RxJS 5 Ultimate",
"url": "https://www.gitbook.com/book/chrisnoring/rxjs-5-ultimate/details"
},
"vsavkin-angular-router": {
"desc": "This book is a comprehensive guide to the Angular router written by its designer. The book explores the library in depth, including the mental model, design constraints, subtleties of the API.",
"rev": true,
"title": "Angular Router",
"url": "https://leanpub.com/router"
},
"vsavkin-essential-angular": {
"desc": "The book is a short, but at the same time, fairly complete overview of the key aspects of Angular written by its core contributors Victor Savkin and Jeff Cross. The book will give you a strong foundation. It will help you put all the concepts into right places. So you will get a good understanding of why the framework is the way it is.",
"rev": true,
"title": "Essential Angular",
"url": "https://gumroad.com/l/essential_angular"
}
}
},
"Online Training": {
"order": 3,
"resources": {
"-KLIBoTWXMiBcvG0dAM6": {
"desc": "This course introduces you to the essentials of this \"superheroic\" framework, including declarative templates, two-way data binding, and dependency injection.",
"rev": true,
"title": "Angular 2: Essential Training",
"url": "https://www.lynda.com/AngularJS-tutorials/Angular-2-Essential-Training/540347-2.html"
},
"-KLIzGq3CiFeoZUemVyE": {
"desc": "Learn the core concepts, play with the code, become a competent Angular 2 developer",
"rev": true,
"title": "Angular 2 Concepts, Code and Collective Wisdom",
"url": "https://www.udemy.com/angular-2-concepts-code-and-collective-wisdom/"
},
"-KLIzHwg-glQLXni1hvL": {
"desc": "Spanish language Angular articles and information",
"rev": true,
"title": "Academia Binaria (español)",
"url": "http://academia-binaria.com/"
},
"-KLIzIOgdPXzI4LMOzYP": {
"desc": "In this course, you will learn the features listed above and so much more. This amazing Angular 2 tutorial will cover the fundamentals of Angular 2 (you dont even need to know Angular), TypeScript, and introduction to the programming concepts such as conditions, arrays, functions, directives, pipes, etc.",
"rev": true,
"title": "Eduonix Angular 2 Fundamentals",
"url": "https://www.eduonix.com/courses/Web-Development/angular-2-fundamentals-for-web-developers"
},
"-KMUuOWwciL_S_o0fzFO": {
"desc": "The ngmigrate project is brought to you by Todd Motto, a Developer Advocate at Telerik, spreading the good word of Kendo UI, NativeScript and Angular 1 & 2. You can follow him on Twitter for questions, or even requests about this guide.",
"rev": true,
"title": "ngMigrate",
"url": "http://ngmigrate.telerik.com/"
},
"-KN3uNQvxifu26D6WKJW": {
"category": "Education",
"desc": "Create the future of web applications by taking Angular 2 for a test drive.",
"rev": true,
"subcategory": "Online Training",
"title": "CodeSchool: Accelerating Through Angular 2",
"url": "https://www.codeschool.com/courses/accelerating-through-angular-2"
},
"a2b": {
"desc": "Hundreds of Angular courses for all skill levels",
"logo": "",
"rev": true,
"title": "Pluralsight",
"url": "https://www.pluralsight.com/search?q=angular+2&categories=all"
},
"ab": {
"desc": "Take this introduction to Angular 2 course, to learn the fundamentals in just two days, free of charge.",
"logo": "",
"rev": true,
"title": "Rangle.io",
"url": "https://rangle.io/services/javascript-training/training-angular1-angular2-with-ngupgrade/"
},
"ab3": {
"desc": "Angular 2 courses hosted by Udemy",
"logo": "",
"rev": true,
"title": "Udemy",
"url": "https://www.udemy.com/courses/search/?ref=home&src=ukw&q=angular+2&lang=en"
},
"ab4": {
"desc": "Angular 2 Fundamentals and advanced topics focused on Redux Style Angular Applications",
"logo": "",
"rev": true,
"title": "Egghead.io",
"url": "https://egghead.io/technologies/angular2"
},
"ab5": {
"desc": "Build Web Apps with Angular 2 - recorded video content",
"logo": "",
"rev": true,
"title": "Frontend Masters",
"url": "https://frontendmasters.com/courses/angular-2/"
},
"ac6": {
"desc": "French language Angular course covering TypeScript, ES6, Depdendency Injection, Observables, and more.",
"rev": true,
"title": "Wishtack's Angular Course (francais)",
"url": "http://courses.wishtack.com/angular-2/ecmascript-6"
},
"angular-love": {
"desc": "Polish language Angular articles and information",
"rev": true,
"title": "angular.love (Polski)",
"url": "http://www.angular.love/"
},
"angular2forms": {
"desc": "Learn about how to use Reactive Forms with Angular.",
"rev": true,
"title": "Angular 2 Forms: Data Binding and Validation",
"url": "https://www.lynda.com/AngularJS-tutorials/Angular-2-Forms-Data-Binding-Validation/461451-2.html"
},
"learn-angular-fr": {
"desc": "French language Angular content.",
"rev": true,
"title": "Learn Angular (francais)",
"url": "http://www.learn-angular.fr/"
},
"sad200": {
"desc": "Free Angular training delivered by SFEIR in France",
"rev": true,
"title": "SFEIR School (French)",
"url": "https://school.sfeir.com/project/sad-200/"
},
"toddmotto-ultimateangular": {
"desc": "Online courses providing in-depth coverage of the Angular ecosystem, AngularJS, Angular and TypeScript, with functional code samples and a full-featured seed environment. Get a deep understanding of Angular and TypeScript from foundation to functional application, then move on to advanced topics with Todd Motto and collaborators.",
"rev": true,
"title": "Ultimate Angular",
"url": "https://ultimateangular.com/"
}
}
},
"Workshops & Onsite Training": {
"order": 2,
"resources": {
"-KLIBo3ANF3-1B9wxsoB": {
"desc": "Angular 2 Classes from Intertech in Minnesota",
"rev": true,
"title": "Intertech",
"url": "http://www.intertech.com/Training/Web-Dev/AngularJS/AngularJS/Angular-2-Training"
},
"-KLIBoFWStce29UCwkvY": {
"desc": "Private Angular 2 Training and Mentoring",
"rev": true,
"title": "Chariot Solutions",
"url": "http://chariotsolutions.com/course/angular2-workshop-fundamentals-architecture/"
},
"-KLIBoN0p9be3kwC6-ga": {
"desc": "Angular Academy is a 2 day hands-on public course given in-person across Canada!",
"rev": true,
"title": "Angular Academy (Canada)",
"url": "http://www.angularacademy.ca"
},
"-KLIBo_lm-WrK1Sjtt-2": {
"desc": "Basic and Advanced training across Europe in German",
"rev": true,
"title": "TheCodeCampus (German)",
"url": "https://www.thecodecampus.de/#!/angularjs"
},
"-KLIzFhfGKi1xttqJ7Uh": {
"desc": "4 day in-depth Angular 2 training in Israel",
"rev": true,
"title": "ng-course (Israel)",
"url": "http://ng-course.org/"
},
"-KLIzIcRoDq3TzCJWnYc": {
"desc": "Virtual and in-person training in Canada and the US",
"rev": true,
"title": "Web Age Solutions",
"url": "http://www.webagesolutions.com/courses/WA2533-angular-2-programming"
},
"500tech": {
"desc": "Learn from 500Tech, an Angular consultancy in Israel. This course was built by an expert developer, who lives and breathes Angular 2, and has practical experience with real world large scale Angular 2 apps.",
"rev": true,
"title": "Angular Hands-on Course (Israel)",
"url": "http://angular2.courses.500tech.com/"
},
"9ab": {
"desc": "OnSite Training From the Authors of \"Become A Ninja with Angular 2\"",
"rev": true,
"title": "Ninja Squad",
"url": "http://ninja-squad.com/formations/formation-angular2"
},
"a2b": {
"desc": "Angular Boot Camp covers introductory and intermediate content. It includes extensive workshop session, with hands-on help from our experienced developer-trainers. At the end of this class, student are usually able to use AngularJS to make an end-to-end, working application.",
"logo": "",
"rev": true,
"title": "Angular Boot Camp",
"url": "https://angularbootcamp.com"
},
"ab": {
"desc": "With Rangles Custom Training, you can cover Angular 2 in comprehensive detail, on your premises or theirs. Learn directly from Angular 2 experts who will tailor course material to suit your specific application needs.",
"logo": "",
"rev": true,
"title": "Rangle.io",
"url": "http://rangle.io/services/javascript-training/angular2-training/"
},
"ab3": {
"desc": "Trainings & Code Reviews. We help people to get a deep understanding of different technologies through trainings and code reviews. Our services can be arranged online, making it possible to join in from anywhere in the world, or on-site to get the best experience possible.",
"logo": "",
"rev": true,
"title": "Thoughtram",
"url": "http://thoughtram.io/"
}
}
}
}
}
}

View File

@ -1,25 +1,22 @@
import { Component, OnInit } from '@angular/core';
import { Contributor } from './contributors.model';
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ContributorGroup } from './contributors.model';
import { ContributorService } from './contributor.service';
@Component({
selector: `aio-contributor-list`,
selector: 'aio-contributor-list',
template: `
<section *ngFor="let group of groups" class="grid-fluid">
<h4 class="title">{{group}}</h4>
<aio-contributor *ngFor="let person of contributorGroups[group]" [person]="person"></aio-contributor>
<section *ngFor="let group of groups | async" class="grid-fluid">
<h4 class="title">{{group.name}}</h4>
<aio-contributor *ngFor="let person of group.contributors" [person]="person"></aio-contributor>
</section>`
})
export class ContributorListComponent implements OnInit {
contributorGroups = new Map<string, Contributor[]>();
groups: string[];
export class ContributorListComponent {
groups: Observable<ContributorGroup[]>;
constructor(private contributorService: ContributorService) { }
ngOnInit() {
this.contributorService.contributors.subscribe(cgs => {
this.groups = ['Lead', 'Google', 'Community'];
this.contributorGroups = cgs;
});
constructor(private contributorService: ContributorService) {
this.groups = this.contributorService.contributors;
}
}

View File

@ -0,0 +1,118 @@
import { ReflectiveInjector } from '@angular/core';
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { ContributorService } from './contributor.service';
import { Contributor, ContributorGroup } from './contributors.model';
import { Logger } from 'app/shared/logger.service';
describe('ContributorService', () => {
let injector: ReflectiveInjector;
let backend: MockBackend;
let contribService: ContributorService;
function createResponse(body: any) {
return new Response(new ResponseOptions({ body: JSON.stringify(body) }));
}
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
ContributorService,
{ provide: ConnectionBackend, useClass: MockBackend },
{ provide: RequestOptions, useClass: BaseRequestOptions },
Http,
Logger
]);
backend = injector.get(ConnectionBackend);
contribService = injector.get(ContributorService);
});
it('should be creatable', () => {
expect(contribService).toBeTruthy();
});
it('should make a single connection to the server', () => {
expect(backend.connectionsArray.length).toEqual(1);
expect(backend.connectionsArray[0].request.url).toEqual('content/contributors.json');
});
describe('#contributors', () => {
let contribs: ContributorGroup[];
let testData: any;
beforeEach(() => {
testData = getTestContribs();
backend.connectionsArray[0].mockRespond(createResponse(testData));
contribService.contributors.subscribe(results => contribs = results);
});
it('contributors observable should complete', () => {
let completed = false;
contribService.contributors.subscribe(null, null, () => completed = true);
expect(true).toBe(true, 'observable completed');
});
it('should reshape the contributor json to expected result', () => {
const groupNames = contribs.map(g => g.name);
expect(groupNames).toEqual(['Lead', 'Google', 'Community']);
});
it('should have expected "Lead" contribs in order', () => {
const leads = contribs[0];
const actualLeadNames = leads.contributors.map(l => l.name).join(',');
const expectedLeadNames = [testData.igor, testData.misko, testData.naomi].map(l => l.name).join(',');
expect(actualLeadNames).toEqual(expectedLeadNames);
});
});
it('should do WHAT(?) if the request fails');
});
function getTestContribs() {
// tslint:disable:quotemark
return {
"misko": {
"name": "Miško Hevery",
"picture": "misko.jpg",
"twitter": "mhevery",
"website": "http://misko.hevery.com",
"bio": "Miško Hevery is the creator of AngularJS framework.",
"group": "Lead"
},
"igor": {
"name": "Igor Minar",
"picture": "igor-minar.jpg",
"twitter": "IgorMinar",
"website": "https://google.com/+IgorMinar",
"bio": "Igor is a software engineer at Google.",
"group": "Lead"
},
"kara": {
"name": "Kara Erickson",
"picture": "kara-erickson.jpg",
"twitter": "karaforthewin",
"website": "https://github.com/kara",
"bio": "Kara is a software engineer on the Angular team at Google and a co-organizer of the Angular-SF Meetup. ",
"group": "Google"
},
"jeffcross": {
"name": "Jeff Cross",
"picture": "jeff-cross.jpg",
"twitter": "jeffbcross",
"website": "https://twitter.com/jeffbcross",
"bio": "Jeff was one of the earliest core team members on AngularJS.",
"group": "Community"
},
"naomi": {
"name": "Naomi Black",
"picture": "naomi.jpg",
"twitter": "naomitraveller",
"website": "http://google.com/+NaomiBlack",
"bio": "Naomi is Angular's TPM generalist and jack-of-all-trades.",
"group": "Lead"
}
};
}

View File

@ -6,13 +6,14 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishLast';
import { Logger } from 'app/shared/logger.service';
import { Contributor } from './contributors.model';
import { Contributor, ContributorGroup } from './contributors.model';
const contributorsPath = 'content/contributors.json';
const knownGroups = ['Lead', 'Google', 'Community'];
@Injectable()
export class ContributorService {
contributors: Observable<Map<string, Contributor[]>>;
contributors: Observable<ContributorGroup[]>;
constructor(private http: Http, private logger: Logger) {
this.contributors = this.getContributors();
@ -21,24 +22,49 @@ export class ContributorService {
private getContributors() {
const contributors = this.http.get(contributorsPath)
.map(res => res.json())
.map(contribs => {
const contribGroups = new Map<string, Contributor[]>();
// Create group map
.map(contribs => {
const contribMap = new Map<string, Contributor[]>();
Object.keys(contribs).forEach(key => {
const contributor = contribs[key];
const group = contributor.group;
const contribGroup = contribGroups[group];
const contribGroup = contribMap[group];
if (contribGroup) {
contribGroup.push(contributor);
} else {
contribGroups[group] = [contributor];
contribMap[group] = [contributor];
}
});
return contribGroups;
return contribMap;
})
// Flatten group map into sorted group array of sorted contributors
.map(cmap => {
return Object.keys(cmap).map(key => {
const order = knownGroups.indexOf(key);
return {
name: key,
order: order === -1 ? knownGroups.length : order,
contributors: cmap[key].sort(compareContributors)
} as ContributorGroup;
})
.sort(compareGroups);
})
.publishLast();
contributors.connect();
return contributors;
}
}
function compareContributors(l: Contributor, r: Contributor) {
return l.name.toUpperCase() > r.name.toUpperCase() ? 1 : -1;
}
function compareGroups(l: ContributorGroup, r: ContributorGroup) {
return l.order === r.order ?
(l.name > r.name ? 1 : -1) :
l.order > r.order ? 1 : -1;
}

View File

@ -1,3 +1,9 @@
export class ContributorGroup {
name: string;
order: number;
contributors: Contributor[];
}
export class Contributor {
group: string;
name: string;

View File

@ -20,13 +20,15 @@ import { ContributorListComponent } from './contributor/contributor-list.compone
import { ContributorComponent } from './contributor/contributor.component';
import { DocTitleComponent } from './doc-title.component';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
import { ResourceListComponent } from './resource/resource-list.component';
import { ResourceService } from './resource/resource.service';
/** Components that can be embedded in docs
* such as CodeExampleComponent, LiveExampleComponent,...
*/
export const embeddedComponents: any[] = [
ApiListComponent, CodeExampleComponent, CodeTabsComponent,
ContributorListComponent, DocTitleComponent, LiveExampleComponent
ContributorListComponent, DocTitleComponent, LiveExampleComponent, ResourceListComponent
];
/** Injectable class w/ property returning components that can be embedded in docs */
@ -46,7 +48,8 @@ export class EmbeddedComponents {
ContributorService,
CopierService,
EmbeddedComponents,
PrettyPrinter
PrettyPrinter,
ResourceService
],
entryComponents: [ embeddedComponents ]
})

View File

@ -0,0 +1,42 @@
<div class="resources grid-fixed">
<div class="c8">
<div class="l-flex--column">
<div class="showcase" *ngFor="let category of categories">
<header class="c-resource-header">
<a class="h-anchor-offset" id="{{category.id}}"></a>
<h2 class="text-headline text-uppercase">{{category.title}}</h2>
</header>
<div class="shadow-1">
<div *ngFor="let subCategory of category.subCategories">
<a class="h-anchor-offset" id="{{subCategory.id}}"></a>
<h3 class="text-uppercase subcategory-title">{{subCategory.title}}</h3>
<div *ngFor="let resource of subCategory.resources">
<div class="c-resource" *ngIf="resource.rev">
<a class="l-flex--column resource-row-link" target="_blank" [href]="resource.url">
<div>
<h4>{{resource.title}}</h4>
<p class="resource-description">{{resource.desc || 'No Description'}}</p>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="c3">
<div class="c-resource-nav shadow-1 l-flex--column h-affix" [ngClass]="{ 'affix-top': scrollPos > 200 }">
<div class="category" *ngFor="let category of categories">
<a class="category-link h-capitalize" [href]="href(category)">{{category.title}}</a>
<div class="subcategory" *ngFor="let subCategory of category.subCategories">
<a class="subcategory-link" [href]="href(subCategory)">{{subCategory.title}}</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
import { ReflectiveInjector } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ResourceListComponent } from './resource-list.component';
import { ResourceService } from './resource.service';
import { Category } from './resource.model';
// Testing the component class behaviors, independent of its template
// Let e2e tests verify how it displays.
describe('ResourceListComponent', () => {
let injector: ReflectiveInjector;
let location: TestPlatformLocation;
let resourceService: TestResourceService;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
ResourceListComponent,
{provide: PlatformLocation, useClass: TestPlatformLocation },
{provide: ResourceService, useClass: TestResourceService }
]);
location = injector.get(PlatformLocation);
resourceService = injector.get(ResourceService);
});
it('should set the location w/o leading slashes', () => {
location.pathname = '////resources';
const component = getComponent();
expect(component.location).toBe('resources');
});
it('href(id) should return the expected href', () => {
location.pathname = '////resources';
const component = getComponent();
expect(component.href({id: 'foo'})).toBe('resources#foo');
});
it('should set scroll position to zero when no target element', () => {
const component = getComponent();
component.onScroll(undefined);
expect(component.scrollPos).toBe(0);
});
it('should set scroll position to element.scrollTop when that is defined', () => {
const component = getComponent();
component.onScroll({scrollTop: 42});
expect(component.scrollPos).toBe(42);
});
it('should set scroll position to element.body.scrollTop when that is defined', () => {
const component = getComponent();
component.onScroll({body: {scrollTop: 42}});
expect(component.scrollPos).toBe(42);
});
it('should set scroll position to 0 when no target.body.scrollTop defined', () => {
const component = getComponent();
component.onScroll({body: {}});
expect(component.scrollPos).toBe(0);
});
//// Test Helpers ////
function getComponent(): ResourceListComponent { return injector.get(ResourceListComponent); }
class TestPlatformLocation {
pathname = 'resources';
}
class TestResourceService {
categories = of(getTestData);
}
function getTestData(): Category[] {
return []; // Not interested in the data in these tests
}
});

View File

@ -0,0 +1,37 @@
import { Component, HostListener, OnInit } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { Category } from './resource.model';
import { ResourceService } from './resource.service';
@Component({
selector: 'aio-resource-list',
templateUrl: 'resource-list.component.html'
})
export class ResourceListComponent implements OnInit {
categories: Category[];
location: string;
scrollPos = 0;
constructor(
location: PlatformLocation,
private resourceService: ResourceService) {
this.location = location.pathname.replace(/^\/+/, '');
}
href(cat: {id: string}) {
return this.location + '#' + cat.id;
}
ngOnInit() {
// Not using async pipe because cats appear twice in template
// No need to unsubscribe because categories observable completes.
this.resourceService.categories.subscribe(cats => this.categories = cats);
}
@HostListener('window:scroll', ['$event.target'])
onScroll(target: any) {
this.scrollPos = target ? target.scrollTop || target.body.scrollTop || 0 : 0;
}
}

View File

@ -0,0 +1,23 @@
export class Category {
id: string; // "education"
title: string; // "Education"
order: number; // 2
subCategories: SubCategory[];
}
export class SubCategory {
id: string; // "books"
title: string; // "Books"
order: number; // 1
resources: Resource[];
}
export class Resource {
category: string; // "Education"
subCategory: string; // "Books"
id: string; // "-KLI8vJ0ZkvWhqPembZ7"
desc: string; // "This books shows all the steps necessary for the development of SPA"
rev: boolean; // true (always true in the original)
title: string; // "Practical Angular 2",
url: string; // "https://leanpub.com/practical-angular-2"
}

View File

@ -0,0 +1,161 @@
import { ReflectiveInjector } from '@angular/core';
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { ResourceService } from './resource.service';
import { Category, SubCategory, Resource } from './resource.model';
import { Logger } from 'app/shared/logger.service';
describe('ResourceService', () => {
let injector: ReflectiveInjector;
let backend: MockBackend;
let resourceService: ResourceService;
function createResponse(body: any) {
return new Response(new ResponseOptions({ body: JSON.stringify(body) }));
}
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
ResourceService,
{ provide: ConnectionBackend, useClass: MockBackend },
{ provide: RequestOptions, useClass: BaseRequestOptions },
Http,
Logger
]);
backend = injector.get(ConnectionBackend);
resourceService = injector.get(ResourceService);
});
it('should be creatable', () => {
expect(resourceService).toBeTruthy();
});
it('should make a single connection to the server', () => {
expect(backend.connectionsArray.length).toEqual(1);
expect(backend.connectionsArray[0].request.url).toEqual('content/resources.json');
});
describe('#categories', () => {
let categories: Category[];
let testData: any;
beforeEach(() => {
testData = getTestResources();
backend.connectionsArray[0].mockRespond(createResponse(testData));
resourceService.categories.subscribe(results => categories = results);
});
it('categories observable should complete', () => {
let completed = false;
resourceService.categories.subscribe(null, null, () => completed = true);
expect(true).toBe(true, 'observable completed');
});
it('should reshape contributors.json to sorted category array', () => {
const actualIds = categories.map(c => c.id).join(',');
expect(actualIds).toBe('cat-1,cat-3');
});
it('should convert ids to canonical form', () => {
// canonical form is lowercase with dashes for spaces
const cat = categories[1];
const sub = cat.subCategories[0];
const res = sub.resources[0];
expect(cat.id).toBe('cat-3', 'category id');
expect(sub.id).toBe('cat3-subcat2', 'subcat id');
expect(res.id).toBe('cat3-subcat2-res1', 'resources id');
});
it('resource knows its category and sub-category titles', () => {
const cat = categories[1];
const sub = cat.subCategories[0];
const res = sub.resources[0];
expect(res.category).toBe(cat.title, 'category title');
expect(res.subCategory).toBe(sub.title, 'subcategory title');
});
it('should have expected SubCategories of "Cat 3"', () => {
const actualIds = categories[1].subCategories.map(s => s.id).join(',');
expect(actualIds).toBe('cat3-subcat2,cat3-subcat1');
});
it('should have expected sorted resources of "Cat 1:SubCat1"', () => {
const actualIds = categories[0].subCategories[0].resources.map(r => r.id).join(',');
expect(actualIds).toBe('a-a-a,s-s-s,z-z-z');
});
});
it('should do WHAT(?) if the request fails');
});
function getTestResources() {
// tslint:disable:quotemark
return {
"Cat 3": {
"order": 3,
"subCategories": {
"Cat3 SubCat1": {
"order": 2,
"resources": {
"Cat3 SubCat1 Res1": {
"desc": "Meetup in Barcelona, Spain. ",
"rev": true,
"title": "Angular Beers",
"url": "http://www.meetup.com/AngularJS-Beers/"
},
"Cat3 SubCat1 Res2": {
"desc": "Angular Camps in Barcelona, Spain.",
"rev": true,
"title": "Angular Camp",
"url": "http://angularcamp.org/"
}
}
},
"Cat3 SubCat2": {
"order": 1,
"resources": {
"Cat3 SubCat2 Res1": {
"desc": "A community index of components and libraries",
"rev": true,
"title": "Catalog of Angular Components & Libraries",
"url": "https://a/b/c"
}
}
},
}
},
"Cat 1": {
"order": 1,
"subCategories": {
"Cat1 SubCat1": {
"order": 1,
"resources": {
"S S S": {
"desc": "SSS",
"rev": true,
"title": "Sssss",
"url": "http://s/s/s"
},
"A A A": {
"desc": "AAA",
"rev": true,
"title": "Aaaa",
"url": "http://a/a/a"
},
"Z Z Z": {
"desc": "ZZZ",
"rev": true,
"title": "Zzzzz",
"url": "http://z/z/z"
}
}
},
},
}
};
}

View File

@ -0,0 +1,83 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishLast';
import { Logger } from 'app/shared/logger.service';
import { Category, Resource, SubCategory } from './resource.model';
const resourcesPath = 'content/resources.json';
@Injectable()
export class ResourceService {
categories: Observable<Category[]>;
constructor(private http: Http, private logger: Logger) {
this.categories = this.getCategories();
}
private getCategories(): Observable<Category[]> {
const categories = this.http.get(resourcesPath)
.map(res => res.json())
.map(data => mkCategories(data))
.publishLast();
categories.connect();
return categories;
};
}
// Extract sorted Category[] from resource JSON data
function mkCategories(categoryJson: any): Category[] {
return Object.keys(categoryJson).map(catKey => {
const cat = categoryJson[catKey];
return {
id: makeId(catKey),
title: catKey,
order: cat.order,
subCategories: mkSubCategories(cat.subCategories, catKey)
} as Category;
})
.sort(compareCats);
}
// Extract sorted SubCategory[] from JSON category data
function mkSubCategories(subCategoryJson: any, catKey: string): SubCategory[] {
return Object.keys(subCategoryJson).map(subKey => {
const sub = subCategoryJson[subKey];
return {
id: makeId(subKey),
title: subKey,
order: sub.order,
resources: mkResources(sub.resources, subKey, catKey)
} as SubCategory;
})
.sort(compareCats);
}
// Extract sorted Resource[] from JSON subcategory data
function mkResources(resourceJson: any, subKey: string, catKey: string): Resource[] {
return Object.keys(resourceJson).map(resKey => {
const res = resourceJson[resKey];
res.category = catKey;
res.subCategory = subKey;
res.id = makeId(resKey);
return res as Resource;
})
.sort(compareTitles);
}
function compareCats(l: Category | SubCategory, r: Category | SubCategory) {
return l.order === r.order ? compareTitles(l, r) : l.order > r.order ? 1 : -1;
}
function compareTitles(l: {title: string}, r: {title: string}) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}
function makeId(title: string) {
return title.toLowerCase().replace(/\s+/g, '-');
}

View File

@ -51,7 +51,12 @@ describe('NavigationService', () => {
expect(viewsEvents).toEqual([]);
backend.connectionsArray[0].mockRespond(createResponse({ TopBar: [ { url: 'a' }] }));
expect(viewsEvents).toEqual([{ TopBar: [ { url: 'a' }] }]);
});
it('navigationViews observable should complete', () => {
let completed = false;
navService.navigationViews.subscribe(null, null, () => completed = true);
expect(true).toBe(true, 'observable completed');
});
it('should return the same object to all subscribers', () => {

View File

@ -354,7 +354,7 @@ describe('LocationService', () => {
});
it('should call locationChanged with initial URL', () => {
const initialUrl = location.path().replace(/^\/+/, '');
const initialUrl = location.path().replace(/^\/+/, ''); // strip leading slashes
expect(gaLocationChanged.calls.count()).toBe(1, 'gaService.locationChanged');
const args = gaLocationChanged.calls.first().args;

View File

@ -125,4 +125,8 @@ header.bckground-sky.l-relative {
}
}
}
}
}
.text-uppercase {
text-transform: uppercase;
}

View File

@ -21,4 +21,5 @@
@import 'hr';
@import 'live-example';
@import 'scrollbar';
@import 'callout';
@import 'callout';
@import 'resources';

View File

@ -0,0 +1,238 @@
.text-headline {
margin: 0px 0px ($unit * 2) 0px;
font-size: 24px;
font-weight: 400;
line-height: 32px;
}
.grid-fixed {
margin: 0 auto;
*zoom: 1;
width: 960px;
}
.grid-fixed .c3, .grid-fixed .c8, {
display: inline;
margin-left: 10px;
margin-right: 10px;
}
.grid-fixed:after, .grid-fixed:before {
content: '.';
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
font-size: 0;
line-height: 0;
width: 0;
height: 0;
}
.grid-fixed .c3 {
width: 220px;
}
.grid-fixed .c8 {
width: 620px;
}
@media handheld and (max-width: 480px), screen and (max-width: 480px), screen and (max-width: 900px) {
.grid-fixed {
width: auto;
}
}
@media handheld and (max-width: 480px), screen and (max-width: 480px), screen and (max-width: 900px) {
.grid-fixed .c3, .grid-fixed .c8 {
margin-left: 20px;
margin-right: 20px;
float: none;
display: block;
width: auto;
}
}
@media handheld and (max-width: 480px), screen and (max-width: 480px), screen and (max-width: 480px) {
.grid-fixed .c3, .grid-fixed .c8 {
margin-left: 0px;
margin-right: 0px;
float: none;
display: block;
width: auto;
}
}
@media handheld and (max-width: 900px), screen and (max-width: 900px) {
/* line 6, ../scss/_responsive.scss */
.grid-fixed{
margin: 0 auto;
*zoom: 1;
}
.grid-fixed:after, .grid-fixed:before, {
content: '.';
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
font-size: 0;
line-height: 0;
width: 0;
height: 0;
}
}
@media handheld and (max-width: 480px), screen and (max-width: 480px) {
/* line 6, ../scss/_responsive.scss */
.grid-fixed {
margin: 0 auto;
*zoom: 1;
}
.grid-fixed:after, .grid-fixed:before {
content: '.';
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
font-size: 0;
line-height: 0;
width: 0;
height: 0;
}
}
.resources {
.shadow-1 {
transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 4px 0 rgba($black, 0.37);
}
.showcase {
margin-bottom: $unit * 6;
border-radius: 4px;
}
.h-affix {
position: fixed;
}
.affix-top {
top: 150px;
}
.c-resource {
h4 {
margin: 0;
line-height: 24px;
}
p {
margin: 0;
}
}
.c-resource-nav {
margin-top: 48px;
width: $unit * 20;
z-index: 1;
background-color: #fff;
border-radius: 2px;
a {
color: #373E41;
text-decoration: none;
}
.category {
padding: 10px 0;
.category-link {
display: block;
margin: 2px 0;
padding: 3px 14px;
font-size: 18px !important;
&:hover {
background: #edf0f2;
color: #2B85E7;
}
}
}
.subcategory {
.subcategory-link {
display: block;
margin: 2px 0;
padding: 4px 14px;
&:hover {
background: #edf0f2;
color: #2B85E7;
}
}
}
}
.h-anchor-offset {
display: block;
position: relative;
top: -20px;
visibility: hidden;
}
.l-flex--column {
display: flex;
flex-direction: column;
}
.c-resource-header {
margin-bottom: 16px;
}
.c-contribute {
margin-bottom: 24px;
}
.c-resource-header h2 {
margin: 0;
}
.subcategory-title {
padding: 16px 23px;
margin: 0;
background-color: $mist;
color: #373E41;
}
.h-capitalize {
text-transform: capitalize;
}
.h-hide {
display: none;
}
.resource-row-link {
color: #1a2326;
border: transparent solid 1px;
margin: 0;
padding: 16px 23px 16px 23px;
position: relative;
text-decoration: none;
transition: all .3s;
}
.resource-row-link:hover {
color: #1a2326;
text-decoration: none;
border-color: #2B85E7;
border-radius: 4px;
box-shadow: 0 8px 8px rgba(1, 67, 163, .24), 0 0 8px rgba(1, 67, 163, .12), 0 6px 18px rgba(43, 133, 231, .12);
transform: translate3d(0, -2px, 0);
}
@media(max-width: 900px) {
.c-resource-nav {
display: none;
}
}
}

View File

@ -14,6 +14,7 @@ $white: #FFFFFF;
$offwhite: #FAFAFA;
$backgroundgray: #F1F1F1;
$lightgray: #DBDBDB;
$mist: #ECEFF1;
$mediumgray: #7E7E7E;
$darkgray: #333;
$black: #0A1014;

View File

@ -153,6 +153,11 @@ module.exports =
include: CONTENTS_PATH + '/marketing/contributors.json',
fileReader: 'jsonFileReader'
},
{
basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/marketing/resources.json',
fileReader: 'jsonFileReader'
},
];
collectExamples.exampleFolders = ['examples', 'examples'];
@ -278,7 +283,8 @@ module.exports =
outputPathTemplate: '${path}.json'
},
{docTypes: ['navigation-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['contributors-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
{docTypes: ['contributors-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['resources-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
];
})