~sirn/fanboi2

46457bcb07bc0f863fcd77426f0c87539b56548d — Kridsada Thanabulpong 5 years ago 2aacd57
Convert assets to use ECMAScript 5.1 with TypeScript.

As using ECMAScript 6 with TypeScript currently feel a little bit
premature (especially on tooling front.) This change also make
the compiled assets much smaller (by half) since babel-polyfill
is no longer required.
M assets/app/javascripts/app.ts => assets/app/javascripts/app.ts +6 -9
@@ 1,14 1,11 @@
/// <reference path="typings/node/node.d.ts" />
/// <reference path="typings/domready/domready.d.ts" />

import 'babel-polyfill';
import 'dom4';
import domready = require('domready');
import boardSelector = require('./components/board_selector');
import inlineQuote = require('./components/inline_quote');

import BoardSelector from './components/board_selector';
import InlineQuote from './components/inline_quote';


require('domready')(function(): void {
    new BoardSelector('.header');
    new InlineQuote('[data-number]');
domready(function(): void {
    new boardSelector.BoardSelector('.header');
    new inlineQuote.InlineQuote('[data-number]');
});

M assets/app/javascripts/components/board_selector.ts => assets/app/javascripts/components/board_selector.ts +29 -26
@@ 1,31 1,32 @@
/// <reference path="../typings/virtual-dom/virtual-dom.d.ts" />
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />

import * as VirtualDOM from 'virtual-dom';
import Board from "../models/board";
import vdom = require('virtual-dom');
import board = require('../models/board');


class BoardSelectorListView {
    boards: Board[];
    boards: board.Board[];
    show: boolean;

    constructor(boards: Board[], show?: boolean) {
    constructor(boards: board.Board[], show?: boolean) {
        this.boards = boards;
        this.show = !!show;
    }

    render(): VirtualDOM.VNode {
        return VirtualDOM.h('div',
    render(): vdom.VNode {
        return vdom.h('div',
            {className: this.getCssFromState()},
            this.boards.map(function(board: Board): VirtualDOM.VNode {
                return VirtualDOM.h('div', {className: 'cascade'}, [
                    VirtualDOM.h('div', {className: 'container'}, [
                        VirtualDOM.h('div', {className: 'cascade-header'}, [
                            VirtualDOM.h('a',
            this.boards.map(function(board: board.Board): vdom.VNode {
                return vdom.h('div', {className: 'cascade'}, [
                    vdom.h('div', {className: 'container'}, [
                        vdom.h('div', {className: 'cascade-header'}, [
                            vdom.h('a',
                                {href: `/${board.slug}/`},
                                String(board.title)
                            ),
                        ]),
                        VirtualDOM.h('div',
                        vdom.h('div',
                            {className: 'cascade-body'},
                            String(board.description)
                        ),


@@ 44,13 45,13 @@ class BoardSelectorListView {
}


export default class BoardSelector {
export class BoardSelector {
    targetElement: Element;
    buttonNode: VirtualDOM.VNode;
    buttonNode: vdom.VNode;
    buttonElement: Element;
    listNode: VirtualDOM.VNode;
    listNode: vdom.VNode;
    listElement: Element;
    boards: Board[];
    boards: board.Board[];

    constructor(targetSelector: string) {
        this.targetElement = document.querySelector(targetSelector);


@@ 66,7 67,7 @@ export default class BoardSelector {
    private attachList(): void {
        let view = new BoardSelectorListView([]);
        this.listNode = view.render();
        this.listElement = VirtualDOM.create(this.listNode);
        this.listElement = vdom.create(this.listNode);

        document.body.insertBefore(
            this.listElement,


@@ 76,12 77,12 @@ export default class BoardSelector {

    private attachButton(): void {
        let self = this;
        this.buttonNode = VirtualDOM.h('div',
        this.buttonNode = vdom.h('div',
            {className: 'js-board-selector-button'},
            [VirtualDOM.h('a', {'href': '#'}, ['Boards'])]
            [vdom.h('a', {'href': '#'}, ['Boards'])]
        );

        this.buttonElement = VirtualDOM.create(this.buttonNode);
        this.buttonElement = vdom.create(this.buttonNode);
        this.buttonElement.addEventListener('click', function(e) {
            e.preventDefault();
            self.eventButtonClicked();


@@ 97,20 98,22 @@ export default class BoardSelector {
        if (this.boards) {
            this.toggleBoardSelectorListState();
        } else {
            Board.queryAll().then(function(boards: Iterable<Board>): void {
                self.boards = Array.from(boards);
            board.Board.queryAll().then(function(
                boards: Array<board.Board>
            ): void {
                self.boards = boards;
                self.toggleBoardSelectorListState();
            });
        }
    }

    private toggleBoardSelectorListState(): void {
        let state = this.listElement.className.includes('disabled');
        let view = new BoardSelectorListView(Array.from(this.boards), state);
        let state = this.listElement.className.indexOf('disabled') != -1;
        let view = new BoardSelectorListView(this.boards, state);
        let viewNode = view.render();
        let patches = VirtualDOM.diff(this.listNode, viewNode);
        let patches = vdom.diff(this.listNode, viewNode);

        this.listElement = VirtualDOM.patch(this.listElement, patches);
        this.listElement = vdom.patch(this.listElement, patches);
        this.listNode = viewNode;
    }
}
\ No newline at end of file

M assets/app/javascripts/components/inline_quote.ts => assets/app/javascripts/components/inline_quote.ts +65 -62
@@ 1,26 1,26 @@
/// <reference path="../typings/dom4/dom4.d.ts" />

import * as VirtualDOM from 'virtual-dom';
import vdom = require('virtual-dom');
import dateFormatter = require('../utils/date_formatter');

import DateFormatter from '../utils/date_formatter';
import Board from '../models/board';
import Topic from '../models/topic';
import Post from '../models/post';
import board = require('../models/board');
import topic = require('../models/topic');
import post = require('../models/post');


class InlineBoardView {
    board: Board;
    board: board.Board;

    constructor(board: Board) {
    constructor(board: board.Board) {
        this.board = board;
    }

    render(): VirtualDOM.VNode {
        return VirtualDOM.h('div',
    render(): vdom.VNode {
        return vdom.h('div',
            {className: 'js-inline-board'},
            [
                VirtualDOM.h('div', {className: 'cascade'}, [
                    VirtualDOM.h('div', {className: 'container'}, [
                vdom.h('div', {className: 'cascade'}, [
                    vdom.h('div', {className: 'container'}, [
                        InlineBoardView.renderTitle(this.board),
                        InlineBoardView.renderDescription(this.board),
                    ])


@@ 29,14 29,14 @@ class InlineBoardView {
        );
    }

    private static renderTitle(board: Board): VirtualDOM.VNode {
        return VirtualDOM.h('div', {className: 'cascade-header'}, [
    private static renderTitle(board: board.Board): vdom.VNode {
        return vdom.h('div', {className: 'cascade-header'}, [
            String(board.title)
        ]);
    }

    private static renderDescription(board: Board): VirtualDOM.VNode {
        return VirtualDOM.h('div', {className: 'cascade-body'}, [
    private static renderDescription(board: board.Board): vdom.VNode {
        return vdom.h('div', {className: 'cascade-body'}, [
            String(board.description)
        ]);
    }


@@ 44,18 44,18 @@ class InlineBoardView {


class InlineTopicView {
    topic: Topic;
    topic: topic.Topic;

    constructor(topic: Topic) {
    constructor(topic: topic.Topic) {
        this.topic = topic;
    }

    render(): VirtualDOM.VNode {
        return VirtualDOM.h('div',
    render(): vdom.VNode {
        return vdom.h('div',
            {className: 'js-inline-topic'},
            [
                VirtualDOM.h('div', {className: 'topic-header'}, [
                    VirtualDOM.h('div', {className: 'container'}, [
                vdom.h('div', {className: 'topic-header'}, [
                    vdom.h('div', {className: 'container'}, [
                        InlineTopicView.renderTitle(this.topic),
                        InlineTopicView.renderDate(this.topic),
                        InlineTopicView.renderCount(this.topic),


@@ 65,55 65,55 @@ class InlineTopicView {
        );
    }

    private static renderTitle(topic: Topic): VirtualDOM.VNode {
        return VirtualDOM.h('h3', {className: 'topic-header-title'}, [
    private static renderTitle(topic: topic.Topic): vdom.VNode {
        return vdom.h('h3', {className: 'topic-header-title'}, [
            String(topic.title)
        ]);
    }

    private static renderDate(topic: Topic): VirtualDOM.VNode {
    private static renderDate(topic: topic.Topic): vdom.VNode {
        let postedAt = new Date(topic.postedAt);
        let dateFormatter = new DateFormatter(postedAt);
        return VirtualDOM.h('p', {className: 'topic-header-item'}, [
        let formatter = new dateFormatter.DateFormatter(postedAt);
        return vdom.h('p', {className: 'topic-header-item'}, [
            String('Last posted '),
            VirtualDOM.h('strong', {}, [String(dateFormatter)]),
            vdom.h('strong', {}, [String(formatter)]),
        ]);
    }

    private static renderCount(topic: Topic): VirtualDOM.VNode {
        return VirtualDOM.h('p', {className: 'topic-header-item'}, [
    private static renderCount(topic: topic.Topic): vdom.VNode {
        return vdom.h('p', {className: 'topic-header-item'}, [
            String('Total of '),
            VirtualDOM.h('strong', {}, [String(`${topic.postCount} posts`)]),
            vdom.h('strong', {}, [String(`${topic.postCount} posts`)]),
        ]);
    }
}


class InlinePostsView {
    posts: Post[];
    posts: post.Post[];

    constructor(posts: Post[]) {
    constructor(posts: post.Post[]) {
        this.posts = posts;
    }

    render(): VirtualDOM.VNode {
        return VirtualDOM.h('div',
    render(): vdom.VNode {
        return vdom.h('div',
            {className: 'js-inline-post'},
            this.posts.map(function(post: Post): VirtualDOM.VNode {
            this.posts.map(function(post: post.Post): vdom.VNode {
                return InlinePostsView.renderPost(post);
            })
        );
    }

    private static renderPost(post: Post): VirtualDOM.VNode {
        return VirtualDOM.h('div', {className: 'container'}, [
    private static renderPost(post: post.Post): vdom.VNode {
        return vdom.h('div', {className: 'container'}, [
            InlinePostsView.renderHeader(post),
            InlinePostsView.renderBody(post),
        ]);
    }

    private static renderHeader(post: Post): VirtualDOM.VNode {
        return VirtualDOM.h('div', {className: 'post-header'}, [
    private static renderHeader(post: post.Post): vdom.VNode {
        return vdom.h('div', {className: 'post-header'}, [
            InlinePostsView.renderHeaderNumber(post),
            InlinePostsView.renderHeaderName(post),
            InlinePostsView.renderHeaderDate(post),


@@ 121,31 121,31 @@ class InlinePostsView {
        ]);
    }

    private static renderHeaderNumber(post: Post): VirtualDOM.VNode {
    private static renderHeaderNumber(post: post.Post): vdom.VNode {
        let classList = ['post-header-item', 'number'];
        if (post.bumped) { classList.push('bumped'); }
        return VirtualDOM.h('span', {
        return vdom.h('span', {
            className: classList.join(' '),
        }, [String(post.number)]);
    }

    private static renderHeaderName(post: Post): VirtualDOM.VNode {
        return VirtualDOM.h('span', {
    private static renderHeaderName(post: post.Post): vdom.VNode {
        return vdom.h('span', {
            className: 'post-header-item name'
        }, [String(post.name)]);
    }

    private static renderHeaderDate(post: Post): VirtualDOM.VNode {
    private static renderHeaderDate(post: post.Post): vdom.VNode {
        let createdAt = new Date(post.createdAt);
        let dateFormatter = new DateFormatter(createdAt);
        return VirtualDOM.h('span', {
        let formatter = new dateFormatter.DateFormatter(createdAt);
        return vdom.h('span', {
            className: 'post-header-item date'
        }, [String(`Posted ${dateFormatter}`)]);
        }, [String(`Posted ${formatter}`)]);
    }

    private static renderHeaderIdent(post: Post): VirtualDOM.VNode | string {
    private static renderHeaderIdent(post: post.Post): vdom.VNode | string {
        if (post.ident) {
            return VirtualDOM.h('span', {
            return vdom.h('span', {
                className: 'post-header-item ident'
            }, [String(`ID:${post.ident}`)]);
        } else {


@@ 153,8 153,8 @@ class InlinePostsView {
        }
    }

    private static renderBody(post: Post): VirtualDOM.VNode {
        return VirtualDOM.h('div', {
    private static renderBody(post: post.Post): vdom.VNode {
        return vdom.h('div', {
            className: 'post-body',
            innerHTML: post.bodyFormatted,
        }, []);


@@ 172,9 172,9 @@ class InlineQuoteHandler {

    attach(): void {
        let self = this;
        this.render().then(function(node: VirtualDOM.VNode | void) {
        this.render().then(function(node: vdom.VNode | void) {
            if (node) {
                self.quoteElement = VirtualDOM.create(<VirtualDOM.VNode>node);
                self.quoteElement = vdom.create(<vdom.VNode>node);
                document.body.insertBefore(self.quoteElement, null);
            }
        });


@@ 186,30 186,33 @@ class InlineQuoteHandler {
        }
    }

    private render(): Promise<VirtualDOM.VNode | void> {
    private render(): Promise<vdom.VNode | void> {
        let targetElement = this.targetElement;
        let boardSlug = targetElement.getAttribute('data-board');
        let topicId = parseInt(targetElement.getAttribute('data-topic'), 10);
        let number = targetElement.getAttribute('data-number');

        if (boardSlug && !topicId && !number) {
            return Board.querySlug(boardSlug).then(function(board: Board) {
            return board.Board.querySlug(boardSlug).then(function(
                board: board.Board
            ): vdom.VNode {
                if (board) {
                    return new InlineBoardView(board).render();
                }
            });
        } else if (topicId && !number) {
            return Topic.queryId(topicId).then(function(topic: Topic) {
            return topic.Topic.queryId(topicId).then(function(
                topic: topic.Topic
            ): vdom.VNode {
                if (topic) {
                    return new InlineTopicView(topic).render();
                }
            });
        } else if (topicId && number) {
            return Post.queryAll(topicId, number).then(
                function(posts: Iterable<Post>) {
                    let postsArray = Array.from(posts);
                    if (postsArray && postsArray.length) {
                        return new InlinePostsView(postsArray).render();
            return post.Post.queryAll(topicId, number).then(
                function(posts: Array<post.Post>) {
                    if (posts && posts.length) {
                        return new InlinePostsView(posts).render();
                    }
                }
            );


@@ 218,7 221,7 @@ class InlineQuoteHandler {
}


export default class InlineQuote {
export class InlineQuote {
    targetSelector: string;

    constructor(targetSelector: string) {

M assets/app/javascripts/models/board.ts => assets/app/javascripts/models/board.ts +10 -10
@@ 1,10 1,10 @@
/// <reference path="../typings/whatwg-fetch/whatwg-fetch.d.ts" />
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />

import 'whatwg-fetch'
import Topic from "./topic";
import topic = require('./topic');


export default class Board {
export class Board {
    type: string;
    id: number;
    agreements: string;


@@ 36,13 36,13 @@ export default class Board {
        };
    }

    static queryAll(): Promise<Iterable<Board>> {
    static queryAll(): Promise<Array<Board>> {
        return window.fetch('/api/1.0/boards/').
            then(function(resp: Response): any { return resp.json(); }).
            then(function*(boards: any[]): Iterable<Board> {
                for (let board of boards) {
                    yield new Board(board);
                }
            then(function(boards: any[]): Array<Board> {
                return boards.map(function(board: Object) {
                    return new Board(board);
                });
            });
    }



@@ 54,7 54,7 @@ export default class Board {
            });
    }

    getTopics(): Promise<Iterable<Topic>> {
        return Topic.queryAll(this.slug);
    getTopics(): Promise<Array<topic.Topic>> {
        return topic.Topic.queryAll(this.slug);
    }
}

M assets/app/javascripts/models/post.ts => assets/app/javascripts/models/post.ts +7 -8
@@ 1,9 1,8 @@
/// <reference path="../typings/whatwg-fetch/whatwg-fetch.d.ts" />
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />

import 'whatwg-fetch';


export default class Post {
export class Post {
    type: string;
    id: number;
    body: string;


@@ 33,7 32,7 @@ export default class Post {
    static queryAll(
        topicId: number,
        query?: string
    ): Promise<Iterable<Post>> {
    ): Promise<Array<Post>> {
        let entryPoint: string;

        if (query) {


@@ 44,10 43,10 @@ export default class Post {

        return window.fetch(entryPoint).
            then(function(resp: Response): any { return resp.json(); }).
            then(function*(posts: any[]): Iterable<Post> {
                for (let post of posts) {
                    yield new Post(post);
                }
            then(function(posts: any[]): Array<Post> {
                return posts.map(function(post: Object): Post {
                    return new Post(post);
                });
            });
    }
}
\ No newline at end of file

M assets/app/javascripts/models/topic.ts => assets/app/javascripts/models/topic.ts +10 -10
@@ 1,7 1,7 @@
/// <reference path="../typings/whatwg-fetch/whatwg-fetch.d.ts" />
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />

import 'whatwg-fetch'
import Post from "./post";
import post = require('./post');


enum Statuses {


@@ 10,7 10,7 @@ enum Statuses {
    Archived,
}

export default class Topic {
export class Topic {
    type: string;
    id: number;
    boardId: number;


@@ 40,13 40,13 @@ export default class Topic {
        }
    }

    static queryAll(slug: string): Promise<Iterable<Topic>> {
    static queryAll(slug: string): Promise<Array<Topic>> {
        return window.fetch(`/api/1.0/boards/${slug}/topics/`).
            then(function(resp: Response): any { return resp.json(); }).
            then(function*(topics: any[]): Iterable<Topic> {
                for (let topic of topics) {
                    yield new Topic(topic);
                }
            then(function(topics: any[]): Array<Topic> {
                return topics.map(function(topic: Object) {
                    return new Topic(topic);
                });
            });
    }



@@ 58,7 58,7 @@ export default class Topic {
            });
    }

    getPosts(query?: string): Promise<Iterable<Post>> {
        return Post.queryAll(this.id, query);
    getPosts(query?: string): Promise<Array<post.Post>> {
        return post.Post.queryAll(this.id, query);
    }
}
\ No newline at end of file

D assets/app/javascripts/typings/whatwg-fetch/whatwg-fetch.d.ts => assets/app/javascripts/typings/whatwg-fetch/whatwg-fetch.d.ts +0 -83
@@ 1,83 0,0 @@
// Type definitions for fetch API
// Project: https://github.com/github/fetch
// Definitions by: Ryan Graham <https://github.com/ryan-codingintrigue>
// Definitions: https://github.com/borisyankov/DefinitelyTyped

declare class Request extends Body {
	constructor(input: string|Request, init?:RequestInit);
	method: string;
	url: string;
	headers: Headers;
	context: string|RequestContext;
	referrer: string;
	mode: string|RequestMode;
	credentials: string|RequestCredentials;
	cache: string|RequestCache;
}

interface RequestInit {
	method?: string;
	headers?: HeaderInit|{ [index: string]: string };
	body?: BodyInit;
	mode?: string|RequestMode;
	credentials?: string|RequestCredentials;
	cache?: string|RequestCache;
}

declare enum RequestContext {
	"audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch",
	"font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import",
	"internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script",
	"serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker",
	"xmlhttprequest", "xslt"
}
declare enum RequestMode { "same-origin", "no-cors", "cors" }
declare enum RequestCredentials { "omit", "same-origin", "include" }
declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" }

declare class Headers {
	append(name: string, value: string): void;
	delete(name: string):void;
	get(name: string): string;
	getAll(name: string): Array<string>;
	has(name: string): boolean;
	set(name: string, value: string): void;
}

declare class Body {
	bodyUsed: boolean;
	arrayBuffer(): Promise<ArrayBuffer>;
	blob(): Promise<Blob>;
	formData(): Promise<FormData>;
	json(): Promise<any>;
	json<T>(): Promise<T>;
	text(): Promise<string>;
}
declare class Response extends Body {
	constructor(body?: BodyInit, init?: ResponseInit);
	error(): Response;
	redirect(url: string, status: number): Response;
	type: string|ResponseType;
	url: string;
	status: number;
	ok: boolean;
	statusText: string;
	headers: Headers;
	clone(): Response;
}

declare enum ResponseType { "basic", "cors", "default", "error", "opaque" }

interface ResponseInit {
	status: number;
	statusText?: string;
	headers?: HeaderInit;
}

declare type HeaderInit = Headers|Array<string>;
declare type BodyInit = Blob|FormData|string;
declare type RequestInfo = Request|string;

interface Window {
	fetch(url: string|Request, init?: RequestInit): Promise<Response>;
}

M assets/app/javascripts/utils/date_formatter.ts => assets/app/javascripts/utils/date_formatter.ts +1 -1
@@ 13,7 13,7 @@ const monthNames = [
    'Dec',
];

export default class DateFormatter {
export class DateFormatter {
    date: Date;

    constructor(date: Date) {

M gulpfile.js => gulpfile.js +2 -5
@@ 11,7 11,6 @@ var source            = require('vinyl-source-stream');
var buffer            = require('vinyl-buffer');
var browserify        = require('browserify');
var tsify             = require('tsify');
var babelify          = require('babelify');


/* Settings


@@ 39,7 38,6 @@ var paths = {
        assets: 'assets/vendor/assets/*',
        stylesheets: 'assets/vendor/stylesheets/**/*.css',
        javascripts: [
            'assets/vendor/javascripts/polyfill.js',
            'assets/vendor/javascripts/**/*.js'
        ]
    },


@@ 117,8 115,8 @@ gulp.task('styles', [
 * ---------------------------------------------------------------------- */

var externalDependencies = [
    'babel-polyfill',
    'whatwg-fetch',
    'es6-promise',
    'virtual-dom',
    'domready',
    'dom4'


@@ 126,8 124,7 @@ var externalDependencies = [

gulp.task('javascripts/app', function(){
    return browserify({basedir: paths.app.javascripts.base, debug: true}).
        plugin(tsify, {target: 'es6', noImplicitAny: true}).
        transform(babelify, {extensions: ['.ts'], presets: ['es2015']}).
        plugin(tsify).
        require(paths.app.javascripts.entry, {entry: true}).
        external(externalDependencies).
        bundle().

M package.json => package.json +1 -3
@@ 26,11 26,9 @@
    "vinyl-buffer":            "^1.0.0",
    "browserify":              "^12.0.1",
    "tsify":                   "^0.12.2",
    "babelify":                "^7.2.0",
    "babel-preset-es2015":     "^6.0.15",

    "babel-polyfill":          "^6.0.16",
    "whatwg-fetch":            "^0.10.1",
    "es6-promise":             "^3.0.2",
    "virtual-dom":             "^2.1.1",
    "domready":                "^1.0.8",
    "dom4":                    "^1.5.2"

A tsconfig.json => tsconfig.json +12 -0
@@ 0,0 1,12 @@
{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitAny": true
  },
  "exclude": [
    "examples",
    "fanboi2",
    "migration",
    "node_modules"
  ]
}
\ No newline at end of file

M tsd.json => tsd.json +5 -2
@@ 11,8 11,11 @@
    "domready/domready.d.ts": {
      "commit": "35989a3ab4834b8e8b2b56788f1d4fc46a25dc8d"
    },
    "node/node.d.ts": {
      "commit": "35989a3ab4834b8e8b2b56788f1d4fc46a25dc8d"
    "whatwg-fetch/whatwg-fetch.d.ts": {
      "commit": "138ad74b9e8e6c08af7633964962835add4c91e2"
    },
    "es6-promise/es6-promise.d.ts": {
      "commit": "138ad74b9e8e6c08af7633964962835add4c91e2"
    }
  }
}