diff --git a/package.json b/package.json
index 339a638..35de632 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"angular2": "2.0.0-beta.7",
"angular2-universal-preview": "0.55.4",
- "angularfire2": "2.0.0-alpha.9",
+ "angularfire2": "2.0.0-alpha.13",
"css": "2.2.1",
"es6-promise": "3.1.2",
"es6-shim": "0.33.13",
@@ -47,6 +47,6 @@
"ts-node": "0.5.4",
"typescript": "1.7.5",
"typescript-node": "0.1.3",
- "typings": "^0.6.8"
+ "typings": "^0.6.9"
}
}
diff --git a/src/css/core/_header.scss b/src/css/core/_header.scss
index 9975a06..2fd01e3 100755
--- a/src/css/core/_header.scss
+++ b/src/css/core/_header.scss
@@ -58,4 +58,8 @@
margin: 0;
color: #FFF;
text-decoration: none;
+ padding-right: 20px;
+ &:last-child {
+ padding-right: 0;
+ }
}
diff --git a/src/index.ng2.html b/src/index.ng2.html
index f548b20..f663a97 100644
--- a/src/index.ng2.html
+++ b/src/index.ng2.html
@@ -17,6 +17,8 @@
+
+
diff --git a/src/main-server.ts b/src/main-server.ts
index 45338fa..9024049 100644
--- a/src/main-server.ts
+++ b/src/main-server.ts
@@ -7,6 +7,7 @@ import {
ng2engine,
REQUEST_URL
} from 'angular2-universal-preview/dist/server';
+import {FIREBASE_PROVIDERS} from 'angularfire2';
import {provide} from 'angular2/core';
import {APP_BASE_HREF, ROUTER_PROVIDERS} from 'angular2/router';
@@ -32,6 +33,7 @@ app.use('/', (req, res) => {
res.render('index', { App, providers: [
ROUTER_PROVIDERS,
SERVER_LOCATION_PROVIDERS,
+ FIREBASE_PROVIDERS,
provide(REQUEST_URL, {useValue: req.originalUrl}),
provide(APP_BASE_HREF, {useValue: `http://localhost:3000${req.baseUrl}`}),
provide(REQUEST_URL, {useValue: 'http://localhost:3000'}),
diff --git a/src/shared-providers.ts b/src/shared-providers.ts
index ef52dda..e0e4ab3 100644
--- a/src/shared-providers.ts
+++ b/src/shared-providers.ts
@@ -1,14 +1,24 @@
import {provide} from 'angular2/core';
-import {FIREBASE_PROVIDERS, defaultFirebase} from 'angularfire2';
+import {
+ defaultFirebase,
+ firebaseAuthConfig,
+ AuthMethods,
+ AuthProviders
+} from 'angularfire2';
import {AuthService} from './worker/services/Auth';
import {Backend, BackendConfig} from './worker/services/Backend';
import {QuestionService} from './worker/services/QuestionService';
-
+import {AnswerService} from './worker/services/AnswerService';
export const SHARED_PROVIDERS = [
AuthService,
QuestionService,
- FIREBASE_PROVIDERS,
- defaultFirebase('answers-mobile.firebaseio.com')
+ AnswerService,
+ defaultFirebase('answers-mobile.firebaseio.com'),
+ firebaseAuthConfig({
+ method: AuthMethods.Redirect,
+ provider: AuthProviders.Github
+ })
];
+
diff --git a/src/shared/ckeditor_renderer.ts b/src/shared/ckeditor_renderer.ts
new file mode 100644
index 0000000..5b15074
--- /dev/null
+++ b/src/shared/ckeditor_renderer.ts
@@ -0,0 +1,37 @@
+import {RenderService} from './render_service';
+import {Injectable, ElementRef, Injector} from 'angular2/core';
+
+declare var CKEDITOR: any;
+
+@RenderService({
+ methods: [{
+ name: 'init',
+ args: [ElementRef],
+ returnType: boolean
+ }, {
+ name: 'destroy',
+ args: [ElementRef]
+ }]
+})
+@Injectable()
+export class CKEditorRenderer {
+ private _editors = new Map();
+ // NB: For now all RenderServices MUST have a public injector member
+ // This is used by the RenderService annotation to inject things like the the MessageBroker
+ constructor (public injector: Injector) {}
+ init (elem): Promise {
+ let editor = CKEDITOR.replace(elem);
+ editor.on("change", (e) => {
+ elem.value = e.editor.getData();
+ let event = new Event('change');
+ elem.dispatchEvent(event);
+ });
+ this._editors.set(elem, editor);
+ }
+
+ destroy(elem) {
+ let editor = this._editors.get(elem);
+ editor.destroy();
+ this._editors.delete(elem);
+ }
+}
diff --git a/src/shared/render_service.ts b/src/shared/render_service.ts
new file mode 100644
index 0000000..a3a1ab0
--- /dev/null
+++ b/src/shared/render_service.ts
@@ -0,0 +1,100 @@
+import {
+ FnArg,
+ ClientMessageBroker,
+ ClientMessageBrokerFactory,
+ ServiceMessageBrokerFactory,
+ UiArguments,
+ PRIMITIVE
+} from 'angular2/platform/worker_app';
+import {RenderStoreObject} from 'angular2/src/web_workers/shared/serializer';
+import {ElementRef, ElementRef_} from 'angular2/src/core/linker/element_ref';
+
+declare var WorkerGlobalScope;
+
+let num = 1;
+let obj = {};
+let serializationMap = {};
+serializationMap[ElementRef.toString()] = RenderStoreObject;
+serializationMap[obj.constructor.toString()] = PRIMITIVE;
+serializationMap[[].constructor.toString()] = PRIMITIVE;
+serializationMap[false.constructor.toString()] = PRIMITIVE;
+serializationMap["".constructor.toString()] = PRIMITIVE;
+serializationMap[num.constructor.toString()] = PRIMITIVE;
+
+export interface MethodMetadata {
+ name: string,
+ args: [Function],
+ returnType?: Function
+}
+
+export interface RenderServiceMetadata {
+ methods: [MethodMetadata]
+}
+
+export function RenderService (metadata: RenderServiceMetadata) {
+ return function(target) {
+ let channelName = target.toString()
+ if (isWorker()) {
+ let state = {
+ channelName: channelName,
+ broker: null
+ };
+ var nativeMethods = {};
+ for (var i = 0; i < metadata.methods.length; i++) {
+ let method = metadata.methods[i];
+ patchMethod(target, method, state);
+ }
+ } else {
+ target.prototype.ngStartListening = function() {
+ let brokerFactory = this.injector.get(ServiceMessageBrokerFactory);
+ let broker = brokerFactory.createMessageBroker(channelName, true);
+ for (var i = 0; i < metadata.methods.length; i++) {
+ let methodInfo = metadata.methods[i];
+ let methodName = methodInfo.name;
+ let args = metadata.methods[i].args.map((v) =>
+ serializationMap[v.toString()] ? serializationMap[v.toString()] : v);
+ broker.registerMethod(methodName, args,
+ target.prototype[methodName].bind(this), methodInfo.returnType);
+ }
+ }
+ }
+ }
+}
+
+function patchMethod(target: any, methodInfo: MethodMetadata, state: ServiceState) {
+ target.prototype[methodInfo.name] = function() {
+ if (state.broker == null) {
+ let brokerFactory: ClientMessageBrokerFactory =
+ this.injector.get(ClientMessageBrokerFactory);
+ state.broker = brokerFactory.createMessageBroker(state.channelName, true);
+ }
+ let broker = state.broker;
+ let args = [];
+ for (var i = 0; i < arguments.length; i++) {
+ let val = arguments[i];
+ if (val) {
+ let type = methodInfo.args[i];
+ if (type == ElementRef) {
+ val = val.nativeElement;
+ }
+ if (serializationMap[type.toString()]) {
+ type = serializationMap[type.toString()];
+ }
+ args.push(new FnArg(val, type));
+ } else {
+ // val may be null, undefined, etc...
+ args.push(val, PRIMITIVE);
+ }
+ }
+ broker.runOnService(new UiArguments(methodInfo.name, args), methodInfo.returnType);
+ }
+}
+
+function isWorker() {
+ return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
+}
+
+interface ServiceState {
+ channelName: string;
+ broker: ClientMessageBroker
+}
diff --git a/src/ui/main_ui.ts b/src/ui/main_ui.ts
index ac56238..e3ef5f6 100644
--- a/src/ui/main_ui.ts
+++ b/src/ui/main_ui.ts
@@ -7,11 +7,22 @@ import {
} from 'angular2/platform/worker_render';
import {platform, provide} from 'angular2/core';
import {BOOTSTRAP_CHANNEL} from '../shared/channels';
+import {CKEditorRenderer} from '../shared/ckeditor_renderer';
+import {
+ defaultFirebase,
+} from 'angularfire2';
+import {WORKER_RENDER_FIREBASE_PROVIDERS} from 'angularfire2/angularfire2_worker_render';
+// Need to import from providers directly
+// until https://github.com/angular/angularfire2/pull/111 is merged
+import {MessageBasedFirebaseAuth} from 'angularfire2/providers/web_workers/ui/auth';
let appRef = platform([WORKER_RENDER_PLATFORM])
.application([
+ WORKER_RENDER_FIREBASE_PROVIDERS,
+ defaultFirebase('answers-mobile.firebaseio.com'),
WORKER_RENDER_APPLICATION,
WORKER_RENDER_ROUTER,
+ CKEditorRenderer,
provide(WORKER_SCRIPT, {useValue: '/loader.js'})
]);
@@ -22,3 +33,7 @@ bus.from(BOOTSTRAP_CHANNEL).subscribe((message: string) => {
( window).preboot.complete();
}
});
+
+// Need to manually call start until https://github.com/angular/angular/issues/7420 is implemented
+appRef.injector.get(MessageBasedFirebaseAuth).start();
+appRef.injector.get(CKEditorRenderer).ngStartListening();
diff --git a/src/worker/components/AnswerList.ts b/src/worker/components/AnswerList.ts
new file mode 100644
index 0000000..5c3083c
--- /dev/null
+++ b/src/worker/components/AnswerList.ts
@@ -0,0 +1,32 @@
+import {Component, Input} from 'angular2/core';
+import {ROUTER_DIRECTIVES} from 'angular2/router';
+import {Answer} from '../services/AnswerService';
+
+const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
+ 'September', 'October', 'November', 'December'];
+
+@Component({
+ selector: 'answer-list',
+ template: `
+
+
{{ answer.text }}
+
Answered by {{ answer.username }} on {{ timestampToDate (answer.timestamp) }}.
+
+ `,
+ directives: [ROUTER_DIRECTIVES],
+ styles: [`
+ .username {
+ font-size: 12pt;
+ text-align: right;
+ }
+ `]
+})
+export class AnswerList {
+ @Input() answers: Answer[];
+
+ timestampToDate (timestamp: number): string {
+ let date = new Date(timestamp);
+ let month = months[date.getMonth()];
+ return `${month} ${date.getDate()}`;
+ }
+}
diff --git a/src/worker/components/CreateAnswer.ts b/src/worker/components/CreateAnswer.ts
new file mode 100644
index 0000000..6807530
--- /dev/null
+++ b/src/worker/components/CreateAnswer.ts
@@ -0,0 +1,52 @@
+import {Component, Input, ChangeDetectionStrategy} from 'angular2/core';
+import {AnswerService, Answer} from '../services/AnswerService';
+import {FirebaseAuth} from 'angularfire2';
+
+import * as Firebase from 'firebase';
+
+@Component({
+ selector: 'create-answer',
+ template: `
+
+
+ You must login to answer this question.
+
+
+ Add Answer
+
+
+
+
+
+
+ `,
+ styles: [
+ `.new-answer-container {
+ display: flex;
+ flex-direction: column
+ }`
+ ]
+})
+export class CreateAnswer {
+ @Input() questionId: string;
+ newAnswer: Answer = {
+ timestamp: Firebase.ServerValue.TIMESTAMP,
+ text: '',
+ uid: '',
+ username: ''
+ };
+
+ constructor(private _answerService: AnswerService, public auth: FirebaseAuth){
+ this.auth.subscribe ((authData) => {
+ if (authData != null) {
+ this.newAnswer.uid = authData.uid;
+ this.newAnswer.username = authData.github.username;
+ }
+ });
+ }
+
+ addAnswer(){
+ this._answerService.addAnswer(this.questionId, this.newAnswer);
+ this.newAnswer.text = '';
+ }
+}
diff --git a/src/worker/components/header.ts b/src/worker/components/header.ts
index 15318c7..1009d16 100644
--- a/src/worker/components/header.ts
+++ b/src/worker/components/header.ts
@@ -1,6 +1,7 @@
-import {Component} from 'angular2/core';
+import {Component, ChangeDetectionStrategy} from 'angular2/core';
import {Nav} from '../services/Nav';
import {ROUTER_DIRECTIVES} from 'angular2/router';
+import {FirebaseAuth} from 'angularfire2';
@Component({
selector: 'app-header',
@@ -8,15 +9,30 @@ import {ROUTER_DIRECTIVES} from 'angular2/router';
+
+
`,
+ changeDetection: ChangeDetectionStrategy.OnPush,
directives: [ROUTER_DIRECTIVES]
})
export class AppHeader {
- constructor(private nav: Nav){
-
+ constructor(private nav: Nav, public auth: FirebaseAuth){
+ this.auth.subscribe((data) => {
+ console.log('auth data', data);
+ });
}
openSideNav(){
this.nav.open()
}
+
+ login() {
+ this.auth.login()
+ .then(() => console.log("Success!"),
+ (err) => console.error(err));
+ }
+
+ logout() {
+ this.auth.logout();
+ }
}
diff --git a/src/worker/containers/createQuestion.ts b/src/worker/containers/createQuestion.ts
index 39d96de..60c05ab 100644
--- a/src/worker/containers/createQuestion.ts
+++ b/src/worker/containers/createQuestion.ts
@@ -1,34 +1,46 @@
import {Component} from 'angular2/core';
import {QuestionService} from '../services/QuestionService';
import {Router, RouteConfig} from 'angular2/router'
+import {FirebaseAuth} from 'angularfire2';
+import {CKEditorDirective} from '../directives/ckeditor_directive';
@Component({
selector: 'create-question-container',
template: `
-
-
Ask a new question
-
-
`,
styles: [
`.new-question-container {
display: flex;
flex-direction: column
}`
- ]
+ ],
+ directives: [CKEditorDirective]
})
export class CreateQuestionContainer {
- newQuestion = {};
- constructor(private questionService:QuestionService, private router:Router){}
- addQuestion(){
+ newQuestion: any = {};
+ constructor(private questionService:QuestionService, private router:Router,
+ public auth: FirebaseAuth) {}
+ addQuestion() {
this.questionService.addQuestion(this.newQuestion);
this.router.navigate(['../Questions']);
}
+
+ questionTextChanged(val: string) {
+ this.newQuestion.text = val;
+ }
}
diff --git a/src/worker/containers/questionDetail.ts b/src/worker/containers/questionDetail.ts
index c8237a9..d70bfd3 100644
--- a/src/worker/containers/questionDetail.ts
+++ b/src/worker/containers/questionDetail.ts
@@ -1,6 +1,9 @@
import {Component, ChangeDetectionStrategy} from 'angular2/core';
import {QuestionService} from '../services/QuestionService';
import {RouteParams} from 'angular2/router';
+import {AnswerList} from '../components/AnswerList';
+import {CreateAnswer} from '../components/CreateAnswer';
+import {AnswerService} from '../services/AnswerService';
@Component({
selector: 'question-detail-container',
@@ -9,12 +12,21 @@ import {RouteParams} from 'angular2/router';
{{ (question | async)?.title }}
{{ (question | async)?.text }}
+
+
`,
- changeDetection: ChangeDetectionStrategy.OnPush
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ directives: [AnswerList, CreateAnswer]
})
export class QuestionDetailContainer {
question: any;
- constructor(private questionService:QuestionService, params:RouteParams){
- this.question = questionService.getQuestionById(params.get('id'));
+ id: string;
+ answers: any;
+
+ constructor(private _questionService:QuestionService, private _answerService: AnswerService,
+ params:RouteParams){
+ this.id = params.get('id');
+ this.question = this._questionService.getQuestionById(this.id);
+ this.answers = this._answerService.getAnswersByQuestionId(this.id);
}
}
diff --git a/src/worker/containers/questions.ts b/src/worker/containers/questions.ts
index 7a0da67..d9412e1 100644
--- a/src/worker/containers/questions.ts
+++ b/src/worker/containers/questions.ts
@@ -1,13 +1,15 @@
import {Component, ChangeDetectionStrategy} from 'angular2/core';
import {Nav} from '../services/Nav';
import {QuestionService} from '../services/QuestionService';
-import {QuestionList} from '../components/QuestionList'
+import {QuestionList} from '../components/QuestionList';
+import {FirebaseAuth} from 'angularfire2';
@Component({
selector: 'home-container',
template: `
-
Recent Questions
+ Recent Questions
+ Hi {{(auth | async).github.displayName}}, here are some recent questions
`,
@@ -15,5 +17,5 @@ import {QuestionList} from '../components/QuestionList'
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuestionsContainer {
- constructor(private questionService:QuestionService){}
+ constructor(private questionService:QuestionService, public auth: FirebaseAuth){}
}
diff --git a/src/worker/directives/ckeditor_directive.ts b/src/worker/directives/ckeditor_directive.ts
new file mode 100644
index 0000000..6ee7a1b
--- /dev/null
+++ b/src/worker/directives/ckeditor_directive.ts
@@ -0,0 +1,21 @@
+import {Directive, OnInit, OnDestroy, ElementRef} from 'angular2/core';
+import {CKEditorRenderer} from '../../shared/ckeditor_renderer';
+
+@Directive ({
+ selector: '[ckeditor]',
+ providers: [CKEditorRenderer]
+})
+export class CKEditorDirective implements OnInit, OnDestroy {
+ constructor(private _renderer: CKEditorRenderer, private _elem: ElementRef) {}
+
+ ngOnInit() {
+ this._renderer.init(this._elem);
+ }
+
+ ngOnDestroy() {
+ console.log("destroying", this._elem);
+ // TODO: There is a race with destroy where the element can be removed from the
+ // store (I believe only on the render side) before the destroy method has executed there.
+ //this._renderer.destroy(this._elem);
+ }
+}
diff --git a/src/worker/main_worker.ts b/src/worker/main_worker.ts
index e698648..7d6de96 100644
--- a/src/worker/main_worker.ts
+++ b/src/worker/main_worker.ts
@@ -8,6 +8,7 @@ import {platform, provide, ComponentRef, Injector} from 'angular2/core';
import {APP_BASE_HREF} from 'angular2/router';
import {BOOTSTRAP_CHANNEL} from '../shared/channels';
import {SHARED_PROVIDERS} from '../shared-providers';
+import {WORKER_APP_FIREBASE_PROVIDERS} from 'angularfire2/angularfire2_worker_app';
import {App} from './app/app';
@@ -15,6 +16,7 @@ platform([WORKER_APP_PLATFORM])
.asyncApplication(null, [
WORKER_APP_ROUTER,
WORKER_APP_APPLICATION,
+ WORKER_APP_FIREBASE_PROVIDERS,
provide(APP_BASE_HREF, {useValue: '/'}),
SHARED_PROVIDERS
]).then((appRef) => appRef.bootstrap(App).then((compRef: ComponentRef) => {
diff --git a/src/worker/services/AnswerService.ts b/src/worker/services/AnswerService.ts
new file mode 100644
index 0000000..9b17259
--- /dev/null
+++ b/src/worker/services/AnswerService.ts
@@ -0,0 +1,37 @@
+import {AngularFire, FirebaseListObservable} from 'angularfire2';
+import {Injectable} from 'angular2/core';
+const ANSWERS_PATH = '/answers';
+
+@Injectable()
+export class AnswerService {
+ private _answersList: {[key: string]: FirebaseListObservable} = {};
+
+ constructor (private _angularFire: AngularFire) {}
+
+ getAnswersByQuestionId(id:string) {
+ return this._lookupByQuestionId (id)
+ .map(l => l.map(v => v.val()));
+ }
+
+ addAnswer(questionId: string, newAnswer: any) {
+ this._lookupByQuestionId(questionId).add(newAnswer);
+ }
+
+ private _lookupByQuestionId(id: string): FirebaseListObservable {
+ let observable: FirebaseListObservable = null;
+ if (this._answersList[id])
+ observable = this._answersList[id];
+ else {
+ observable = this._angularFire.list(ANSWERS_PATH + `/${id}`, {preserveSnapshot: true});
+ this._answersList[id] = observable;
+ }
+ return observable;
+ }
+}
+
+export interface Answer {
+ text: string;
+ uid: string;
+ timestamp: number;
+ username: string;
+}