├── .firebaserc
├── public
├── cycle.png
├── favicon.ico
├── images
│ ├── apple-icon.png
│ ├── icon-72x72.png
│ ├── icon-96x96.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── icon-128x128.png
│ ├── icon-144x144.png
│ ├── icon-152x152.png
│ ├── icon-192x192.png
│ ├── icon-384x384.png
│ ├── icon-512x512.png
│ ├── ms-icon-70x70.png
│ ├── apple-icon-57x57.png
│ ├── apple-icon-60x60.png
│ ├── apple-icon-72x72.png
│ ├── apple-icon-76x76.png
│ ├── ms-icon-144x144.png
│ ├── ms-icon-150x150.png
│ ├── ms-icon-310x310.png
│ ├── android-icon-36x36.png
│ ├── android-icon-48x48.png
│ ├── android-icon-72x72.png
│ ├── android-icon-96x96.png
│ ├── apple-icon-114x114.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-144x144.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-180x180.png
│ ├── android-icon-144x144.png
│ ├── android-icon-192x192.png
│ └── apple-icon-precomposed.png
└── manifest.json
├── screens
├── cycle-hn1.png
├── cycle-hn2.png
└── lighthouse.png
├── database.rules.json
├── custom-typings.d.ts
├── src
├── pages
│ ├── partials
│ │ └── _pulse.tsx
│ ├── factory.ts
│ ├── FeedView
│ │ ├── view.tsx
│ │ ├── index.tsx
│ │ └── reducer.ts
│ ├── types.ts
│ └── FeedsList
│ │ ├── reducer.ts
│ │ ├── index.tsx
│ │ └── view.tsx
├── components
│ ├── CommentCollection.tsx
│ ├── FeedCollection.tsx
│ ├── CommentAtom.tsx
│ └── FeedAtom.tsx
├── interfaces.ts
├── routes.ts
├── index.ts
├── app.tsx
└── css
│ └── styles.scss
├── tsconfig.json
├── webpack.config.js
├── .gitignore
├── firebase.json
├── LICENSE
├── README.md
├── tslint.json
├── configs
├── webpack.config.test.js
└── webpack.config.js
├── test
└── app.test.ts
├── index.ejs
└── package.json
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "cyclejs-hn"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/cycle.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/screens/cycle-hn1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/screens/cycle-hn1.png
--------------------------------------------------------------------------------
/screens/cycle-hn2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/screens/cycle-hn2.png
--------------------------------------------------------------------------------
/screens/lighthouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/screens/lighthouse.png
--------------------------------------------------------------------------------
/public/images/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon.png
--------------------------------------------------------------------------------
/public/images/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-72x72.png
--------------------------------------------------------------------------------
/public/images/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-96x96.png
--------------------------------------------------------------------------------
/public/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/favicon-16x16.png
--------------------------------------------------------------------------------
/public/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/favicon-32x32.png
--------------------------------------------------------------------------------
/public/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/favicon-96x96.png
--------------------------------------------------------------------------------
/public/images/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-128x128.png
--------------------------------------------------------------------------------
/public/images/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-144x144.png
--------------------------------------------------------------------------------
/public/images/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-152x152.png
--------------------------------------------------------------------------------
/public/images/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-192x192.png
--------------------------------------------------------------------------------
/public/images/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-384x384.png
--------------------------------------------------------------------------------
/public/images/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/icon-512x512.png
--------------------------------------------------------------------------------
/public/images/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/ms-icon-70x70.png
--------------------------------------------------------------------------------
/public/images/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-57x57.png
--------------------------------------------------------------------------------
/public/images/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-60x60.png
--------------------------------------------------------------------------------
/public/images/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-72x72.png
--------------------------------------------------------------------------------
/public/images/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-76x76.png
--------------------------------------------------------------------------------
/public/images/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/ms-icon-144x144.png
--------------------------------------------------------------------------------
/public/images/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/ms-icon-150x150.png
--------------------------------------------------------------------------------
/public/images/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/ms-icon-310x310.png
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | ".read": "auth != null",
4 | ".write": "auth != null"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-36x36.png
--------------------------------------------------------------------------------
/public/images/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-48x48.png
--------------------------------------------------------------------------------
/public/images/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-72x72.png
--------------------------------------------------------------------------------
/public/images/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-96x96.png
--------------------------------------------------------------------------------
/public/images/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-114x114.png
--------------------------------------------------------------------------------
/public/images/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-120x120.png
--------------------------------------------------------------------------------
/public/images/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-144x144.png
--------------------------------------------------------------------------------
/public/images/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-152x152.png
--------------------------------------------------------------------------------
/public/images/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-180x180.png
--------------------------------------------------------------------------------
/public/images/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-144x144.png
--------------------------------------------------------------------------------
/public/images/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/android-icon-192x192.png
--------------------------------------------------------------------------------
/public/images/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usm4n/cycle-hn/HEAD/public/images/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/custom-typings.d.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line
2 | ///
3 | declare module 'cycle-restart';
4 |
5 | declare var Snabbdom: any; //Automaticly imported into every file
6 |
--------------------------------------------------------------------------------
/src/pages/partials/_pulse.tsx:
--------------------------------------------------------------------------------
1 | import { VNode } from '@cycle/dom';
2 |
3 | export function pulse(): VNode {
4 | return (
5 |
11 | );
12 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "module": "ES6",
5 | "moduleResolution": "node",
6 | "target": "ES5",
7 | "sourceMap": true,
8 | "jsx": "react",
9 | "jsxFactory": "Snabbdom.createElement",
10 | "noImplicitReturns": true,
11 | "strict": true,
12 | "strictFunctionTypes": false,
13 | "allowJs": true,
14 | "lib": ["es6", "es5", "dom"]
15 | },
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const appPath = (...names) => path.join(process.cwd(), ...names);
4 |
5 | //This will be merged with the config from the flavor
6 | module.exports = {
7 | entry: {
8 | main: [appPath('src', 'index.ts'), appPath('src', 'css', 'styles.scss')]
9 | },
10 | output: {
11 | filename: 'bundle.[hash:8].js',
12 | publicPath: '/',
13 | path: appPath('build')
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/CommentCollection.tsx:
--------------------------------------------------------------------------------
1 | import xs, { Stream } from 'xstream';
2 | import { StateSource, makeCollection } from 'cycle-onionify';
3 | import { Component, Sources, Sinks } from '../interfaces';
4 | import { CommentAtom, State as CommentState } from './CommentAtom';
5 |
6 | export const CommentCollection: Component = makeCollection({
7 | item: CommentAtom,
8 | itemKey: state => String(state.id),
9 | itemScope: key => key,
10 | collectSinks: instances => ({
11 | DOM: instances.pickCombine('DOM')
12 | })
13 | });
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Specifies intentionally untracked files to ignore when using Git
2 | # http://git-scm.com/docs/gitignore
3 |
4 | *~
5 | *.sw[mnpcod]
6 | *.log
7 | *.tmp
8 | *.tmp.*
9 | log.txt
10 | *.sublime-project
11 | *.sublime-workspace
12 | .vscode/
13 | npm-debug.log*
14 |
15 | .idea/
16 | .sass-cache/
17 | .tmp/
18 | .versions/
19 | coverage/
20 | dist/
21 | build/
22 | node_modules/
23 | tmp/
24 | temp/
25 | hooks/
26 | platforms/
27 | plugins/
28 | plugins/android.json
29 | plugins/ios.json
30 | www/
31 | $RECYCLE.BIN/
32 |
33 | .DS_Store
34 | Thumbs.db
35 | UserInterfaceState.xcuserstate
36 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "hosting": {
6 | "public": "build",
7 | "ignore": [
8 | "firebase.json",
9 | "**/.*",
10 | "**/node_modules/**"
11 | ],
12 | "rewrites": [
13 | {
14 | "source": "**",
15 | "destination": "/index.html"
16 | }
17 | ],
18 | "headers": [{
19 | "source": "**/*.@(jpg|jpeg|gif|png|js|css|json|svg|ico|scss|html)",
20 | "headers": [{
21 | "key": "Cache-Control",
22 | "value": "max-age=31536000"
23 | }]
24 | }]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/FeedCollection.tsx:
--------------------------------------------------------------------------------
1 | import xs, { Stream } from 'xstream';
2 | import { StateSource, makeCollection } from 'cycle-onionify';
3 | import { Component, Sources, Sinks } from '../interfaces';
4 | import { FeedAtom, State as FeedState } from './FeedAtom';
5 |
6 | export const FeedsCollection: Component = makeCollection({
7 | item: FeedAtom,
8 | itemKey: state => String(state.id),
9 | itemScope: key => key,
10 | collectSinks: instances => ({
11 | DOM: instances.pickCombine('DOM').map(itemNodes =>
12 | itemNodes.map(item => ({...item, sel: 'li'})
13 | ))
14 | })
15 | });
16 |
--------------------------------------------------------------------------------
/src/pages/factory.ts:
--------------------------------------------------------------------------------
1 | import xs from 'xstream';
2 | import FeedVeiw from './FeedView';
3 | import FeedView from './FeedView';
4 | import FeedsList from './FeedsList';
5 | import { Component } from '../interfaces';
6 | import { PageSinks, PageSources, PageParams } from './types';
7 |
8 | export function createPage(page: string, params: PageParams): Component {
9 | const params$ = xs.of(params);
10 |
11 | return (sources: PageSources): PageSinks => {
12 | const pageSources: PageSources = {params$, ...sources};
13 |
14 | if (page === 'atom') {
15 | return FeedView(pageSources);
16 | } else {
17 | return FeedsList(pageSources);
18 | }
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { Stream } from 'xstream';
2 | import { TimeSource } from '@cycle/time';
3 | import { VNode, DOMSource } from '@cycle/dom';
4 | import { HTTPSource, RequestOptions } from '@cycle/http';
5 | import { Location } from 'history';
6 | import { HistoryInput, HistoryDriver } from '@cycle/history';
7 |
8 | export type Sources = {
9 | DOM: DOMSource;
10 | HTTP: HTTPSource;
11 | Time: TimeSource;
12 | History: Stream,
13 | };
14 |
15 | export type RootSinks = {
16 | DOM: Stream;
17 | HTTP: Stream;
18 | History: Stream
19 | };
20 |
21 | export type Sinks = Partial;
22 | export type Component = (s: Sources) => Sinks;
23 |
--------------------------------------------------------------------------------
/src/pages/FeedView/view.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | PageState,
3 | FeedViewState as AtomState
4 | } from '../types';
5 | import { VNode } from '@cycle/dom';
6 | import xs, { Stream } from 'xstream';
7 | import { pulse } from '../partials/_pulse';
8 |
9 | function content(feedDom: VNode, commentsDom: VNode): VNode {
10 | return ();
18 | }
19 | export function view(state$: Stream, feedDom$: Stream, commentsDom$: Stream): Stream {
20 | return xs.combine(state$, feedDom$, commentsDom$)
21 | .map(([state, feed, comments]) => {
22 | if (!state.isLoading) {
23 | return content(feed, comments);
24 | } else {
25 | return pulse();
26 | }
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/types.ts:
--------------------------------------------------------------------------------
1 | import { Stream } from 'xstream';
2 | import { StateSource } from 'cycle-onionify';
3 | import { Sources, Sinks } from '../interfaces';
4 | import { State as FeedState } from '../components/FeedAtom';
5 | import { State as CommentState } from '../components/CommentAtom';
6 |
7 | export interface PageParams {
8 | id?: string;
9 | page?: string;
10 | [param: string]: any;
11 | }
12 |
13 | export interface PulseLoader {
14 | show: boolean;
15 | }
16 |
17 | interface PageBase {
18 | meta?: PageParams;
19 | isLoading: boolean;
20 | // pulse: PulseLoader;
21 | }
22 |
23 | export interface FeedViewState extends PageBase {
24 | feed: FeedState;
25 | comments: Array;
26 | }
27 |
28 | export interface FeedsListState extends PageBase {
29 | feeds: Array;
30 | }
31 |
32 | export type PageState = FeedsListState | FeedViewState;
33 |
34 | export type PageSinks = Sinks & { onion: Stream };
35 | export type PageReducer = (prev?: PageState) => PageState | undefined;
36 | export type PageSources = Sources & { onion: StateSource, params$: Stream};
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Usman Riaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/pages/FeedsList/reducer.ts:
--------------------------------------------------------------------------------
1 | import isolate from '@cycle/isolate';
2 | import xs, { Stream } from 'xstream';
3 | import {
4 | PageState,
5 | PageSources as Sources,
6 | PageReducer as Reducer
7 | } from '../types';
8 | import { State as FeedState } from '../../components/FeedAtom';
9 |
10 | const defaultState: PageState = {
11 | isLoading: true,
12 | meta: {
13 | max: 0,
14 | page: '0',
15 | type: 'news'
16 | },
17 | feeds: [] as Array
18 | };
19 |
20 | export function makeReducer$(sources: Sources): Stream {
21 | const params$ = sources.params$;
22 | const http$ = sources.HTTP.select('feeds').flatten() ;
23 |
24 | // reset any residual state
25 | const initReducer$ = xs.of(() => defaultState);
26 |
27 | const pageReducer$ = xs.combine(http$, params$)
28 | .map(([res, params]): any => ({feeds: res.body, params}))
29 | .map(pageData => function(state: PageState): PageState {
30 | return {
31 | ...state,
32 | isLoading: false,
33 | meta: pageData.params,
34 | feeds: pageData.feeds
35 | };
36 | } as Reducer);
37 |
38 | return xs.merge(initReducer$, pageReducer$);
39 | }
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CycleJS Hackernews",
3 | "short_name": "Cycle HN",
4 | "theme_color": "#24242d",
5 | "background_color": "#f3f6fb",
6 | "display": "standalone",
7 | "orientation": "portrait",
8 | "Scope": "/",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "images/icon-72x72.png",
13 | "sizes": "72x72",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "images/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "images/icon-128x128.png",
23 | "sizes": "128x128",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "images/icon-144x144.png",
28 | "sizes": "144x144",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "images/icon-152x152.png",
33 | "sizes": "152x152",
34 | "type": "image/png"
35 | },
36 | {
37 | "src": "images/icon-192x192.png",
38 | "sizes": "192x192",
39 | "type": "image/png"
40 | },
41 | {
42 | "src": "images/icon-384x384.png",
43 | "sizes": "384x384",
44 | "type": "image/png"
45 | },
46 | {
47 | "src": "images/icon-512x512.png",
48 | "sizes": "512x512",
49 | "type": "image/png"
50 | }
51 | ],
52 | "splash_pages": null
53 | }
--------------------------------------------------------------------------------
/src/pages/FeedsList/index.tsx:
--------------------------------------------------------------------------------
1 | import { view } from './view';
2 | import { VNode } from '@cycle/dom';
3 | import { API_URL } from '../../app';
4 | import isolate from '@cycle/isolate';
5 | import xs, { Stream } from 'xstream';
6 | import {
7 | PageSinks as Sinks,
8 | PageSources as Sources,
9 | PageReducer as Reducer
10 | } from '../types';
11 | import { makeReducer$ } from './reducer';
12 | import { HTTPSource, RequestOptions } from '@cycle/http';
13 | import { FeedsCollection } from '../../components/FeedCollection';
14 |
15 | function requestMapper({page, type}: {page: string, type: string}): RequestOptions {
16 | return {
17 | method: 'GET',
18 | category: 'feeds',
19 | query: {page},
20 | url: API_URL + `/${type}.json`
21 | };
22 | }
23 |
24 | export default function FeedsList(sources: Sources): Sinks {
25 | const state$ = sources.onion.state$;
26 |
27 | const request$ = sources.params$.map(requestMapper);
28 |
29 | const reducers$: Stream = makeReducer$(sources);
30 | const feedsCollection = isolate(FeedsCollection, 'feeds')(sources);
31 |
32 | const feedsDom$ = feedsCollection.DOM;
33 | const pageDom$: Stream = view(state$, feedsDom$);
34 |
35 | const sinks = {
36 | DOM: pageDom$,
37 | HTTP: request$,
38 | onion: reducers$
39 | };
40 |
41 | return sinks;
42 | }
43 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './interfaces';
2 | import { PageParams } from './pages/types';
3 | import { createPage } from './pages/factory';
4 |
5 | export interface RouteDefinition {
6 | [path: string]: RouteDefinition | any;
7 | }
8 |
9 | export interface MatchedRoute {
10 | path: string | null;
11 | value: Component | any;
12 | }
13 |
14 | export const Routes: RouteDefinition = {
15 | '/': createPage('list', {max: 10, type: 'news', page: '1'}),
16 | '/news': createPage('list', {max: 10, type: 'news', page: '1'}),
17 | '/news/:page': (page: string) => createPage('list', {max: 10, type: 'news', page}),
18 | '/newest': createPage('list', {max: 12, type: 'newest', page: '1'}),
19 | '/newest/:page': (page: string) => createPage('list', {max: 12, type: 'newest', page}),
20 | '/show': createPage('list', {max: 2, type: 'show', page: '1'}),
21 | '/show/:page': (page: string) => createPage('list', {max: 2, type: 'show', page}),
22 | '/ask': createPage('list', {max: 3, type: 'ask', page: '1'}),
23 | '/ask/:page': (page: string) => createPage('list', {max: 3, type: 'ask', page}),
24 | '/jobs': createPage('list', {max: 1, type: 'show', page: '1'}),
25 | '/jobs/:page': (page: string) => createPage('list', {max: 1, type: 'jobs', page}),
26 | '/atom/:id': (id: string) => createPage('atom', {id, type: 'item'}),
27 | '*': createPage('list', {max: 10, type: 'news', page: '1'})
28 | };
29 |
--------------------------------------------------------------------------------
/src/pages/FeedView/index.tsx:
--------------------------------------------------------------------------------
1 | import { view } from './view';
2 | import { VNode } from '@cycle/dom';
3 | import { API_URL } from '../../app';
4 | import isolate from '@cycle/isolate';
5 | import xs, { Stream } from 'xstream';
6 | import {
7 | PageSinks as Sinks,
8 | PageSources as Sources,
9 | PageReducer as Reducer
10 | } from '../types';
11 | import { makeReducer$ } from './reducer';
12 | import { FeedAtom } from '../../components/FeedAtom';
13 | import { HTTPSource, RequestOptions } from '@cycle/http';
14 | import { CommentCollection } from '../../components/CommentCollection';
15 |
16 | function requestMapper({id, type}: {id: string, type: string}): RequestOptions {
17 | return {
18 | method: 'GET',
19 | category: 'atom',
20 | url: API_URL + `/${type}/${id}.json`
21 | };
22 | }
23 |
24 | export default function FeedsView(sources: Sources): Sinks {
25 | const state$ = sources.onion.state$;
26 |
27 | const request$ = sources.params$.map(requestMapper);
28 |
29 | const reducers$: Stream = makeReducer$(sources);
30 | const feedAtom = isolate(FeedAtom, 'feed')(sources);
31 | const commentCollection = isolate(CommentCollection, 'comments')(sources);
32 |
33 | const pageDom$: Stream = view(state$, feedAtom.DOM, commentCollection.DOM);
34 |
35 | const sinks = {
36 | DOM: pageDom$,
37 | HTTP: request$,
38 | onion: reducers$
39 | };
40 |
41 | return sinks;
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/FeedView/reducer.ts:
--------------------------------------------------------------------------------
1 | import isolate from '@cycle/isolate';
2 | import xs, { Stream } from 'xstream';
3 | import {
4 | PageState,
5 | PageSources as Sources,
6 | PageReducer as Reducer
7 | } from '../types';
8 | import { State as FeedState } from '../../components/FeedAtom';
9 | import { State as CommentState } from '../../components/CommentAtom';
10 |
11 | const defaultState: PageState = {
12 | isLoading: true,
13 | feed: {} as FeedState,
14 | comments: {} as Array
15 | };
16 |
17 | function extractFeed(data: any): FeedState {
18 | const keys: Array = Object.keys(data);
19 |
20 | return keys.reduce((feed: FeedState, key: string) => {
21 | if (key !== 'comments') {
22 | feed[key] = data[key];
23 | }
24 |
25 | return feed;
26 | }, {} as FeedState);
27 | }
28 |
29 | export function makeReducer$(sources: Sources): Stream {
30 | const http$ = sources.HTTP.select('atom').flatten() ;
31 |
32 | const initReducer$ = xs.of(() => defaultState);
33 |
34 | const pageReducer$ = http$.map((res: any) => res.body)
35 | .map(pageData => function(state: PageState): PageState {
36 | return {
37 | ...state,
38 | isLoading: false,
39 | feed: extractFeed(pageData),
40 | comments: pageData.comments
41 | };
42 | } as Reducer);
43 |
44 | return xs.merge(initReducer$, pageReducer$);
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # [cycle-hn](https://github.com/usm4n/cycle-hn)
4 |
5 | A [CycleJS](https://github.com/cyclejs/cyclejs) &
6 | [cycle-onionify](https://github.com/staltz/cycle-onionify)-powered implementation of
7 | [Hacker News](https://news.ycombinator.com) using its
8 | [HNPWA API](https://github.com/tastejs/hacker-news-pwas/blob/master/docs/api.md).
9 |
10 | ## Screenshot
11 |
12 | 
13 |
14 | 
15 |
16 | ### Lighthouse stats
17 | 
18 |
19 | Live version:
20 |
21 | [@Now](https://build-mnrpnfoxlt.now.sh) - preferred
22 |
23 | [@Firebaseapp](https://cyclejs-hn.firebaseapp.com)
24 |
25 | [Feature requests are welcome!](https://github.com/usm4n/cycle-hn/issues/new)
26 |
27 | ## Building
28 |
29 | Install dependencies:
30 |
31 | ```
32 | npm install
33 | ```
34 |
35 | ### npm scripts
36 |
37 | * `npm start` - start development server
38 | * `npm run build` - build into the `build/` directory
39 |
40 | ## Acknowledgement
41 | [Jan van Brügge](https://github.com/jvanbruegge) for awesome [one-fits-all](https://github.com/cyclejs-community/one-fits-all) flavor for [cycle-scripts](https://github.com/cyclejs-community/create-cycle-app)
42 |
43 |
44 | ### License
45 | Cycle-hn is a free software realeased under the terms of MIT license.
46 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "member-access": true,
4 | "no-reference": true,
5 | "typedef": [
6 | true,
7 | "call-signature",
8 | "call-signature",
9 | "parameter",
10 | "property-declaration",
11 | "member-variable-declaration"
12 | ],
13 | "typedef-whitespace": [
14 | true,
15 | {
16 | "call-signature": "nospace",
17 | "index-signature": "nospace",
18 | "parameter": "nospace",
19 | "property-declaration": "nospace",
20 | "variable-declaration": "nospace"
21 | },
22 | {
23 | "call-signature": "onespace",
24 | "index-signature": "onespace",
25 | "parameter": "onespace",
26 | "property-declaration": "onespace",
27 | "variable-declaration": "onespace"
28 | }
29 | ],
30 | "curly": true,
31 | "no-duplicate-variable": true,
32 | "no-empty": true,
33 | "no-eval": true,
34 | "no-null-keyword": true,
35 | "no-shadowed-variable": true,
36 | "no-use-before-declare": true,
37 | "no-var-keyword": true,
38 | "switch-default": true,
39 | "triple-equals": true,
40 | "use-isnan": true,
41 | "prefer-const": true,
42 | "trailing-comma": [true, { "multiline": "never", "singleline": "never" }],
43 | "class-name": true,
44 | "no-angle-bracket-type-assertion": true,
45 | "no-consecutive-blank-lines": true,
46 | "quotemark": [true, "single", "jsx-double"],
47 | "semicolon": [true, "always"],
48 | "space-before-function-paren": [true, "never"],
49 | "whitespace": [
50 | true,
51 | "check-decl",
52 | "check-operator",
53 | "check-module",
54 | "check-type"
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import xs from 'xstream';
2 | import isolate from '@cycle/isolate';
3 | import onionify from 'cycle-onionify';
4 | import { setup, run } from '@cycle/run';
5 | import { timeDriver } from '@cycle/time';
6 | import { makeDOMDriver } from '@cycle/dom';
7 | import { makeHTTPDriver } from '@cycle/http';
8 | import { restartable, rerunner } from 'cycle-restart';
9 | import { makeHistoryDriver, captureClicks } from '@cycle/history';
10 |
11 | import { App } from './app';
12 | import { Component, Sources, RootSinks } from './interfaces';
13 |
14 | const main: Component = onionify(App);
15 |
16 | let drivers: any, driverFn: any;
17 | /// #if PRODUCTION
18 | drivers = {
19 | DOM: makeDOMDriver('#app'),
20 | HTTP: makeHTTPDriver(),
21 | Time: timeDriver,
22 | History: captureClicks(makeHistoryDriver())
23 | };
24 | /// #else
25 | driverFn = () => ({
26 | DOM: restartable(makeDOMDriver('#app'), {
27 | pauseSinksWhileReplaying: false
28 | }),
29 | HTTP: restartable(makeHTTPDriver()),
30 | Time: timeDriver,
31 | History: captureClicks(makeHistoryDriver())
32 | });
33 | /// #endif
34 | export const driverNames: string[] = Object.keys(drivers || driverFn());
35 |
36 | /// #if PRODUCTION
37 | run(main as any, drivers);
38 | // register service worker
39 | if ('serviceWorker' in navigator) {
40 | navigator.serviceWorker
41 | .register('/service-worker.js')
42 | .then(registration => console.log('SW registration successful with scope: ', registration.scope));
43 | }
44 | /// #else
45 | const rerun = rerunner(setup, driverFn, isolate);
46 | rerun(main as any);
47 |
48 | if (module.hot) {
49 | module.hot.accept('./app', () => {
50 | const newApp = require('./app').App;
51 |
52 | rerun(onionify(newApp));
53 | });
54 | }
55 | /// #endif
56 |
--------------------------------------------------------------------------------
/configs/webpack.config.test.js:
--------------------------------------------------------------------------------
1 | const config = require('./webpack.config.js');
2 | const nodeExternals = require('webpack-node-externals');
3 |
4 | module.exports = Object.assign({}, config, {
5 | target: 'node',
6 | devtool: 'inline-source-map',
7 | externals: [nodeExternals()],
8 | plugins: config.plugins.filter(p => !(p.options && p.options.template)), //Exclude HtmlWebpackPlugin
9 | module: Object.assign({}, config.module, {
10 | loaders: config.module.loaders.map(l => {
11 | if (
12 | l.loaders &&
13 | l.loaders.reduce(
14 | (acc, curr) =>
15 | acc || /awesome-typescript-loader.*/.test(curr),
16 | false
17 | )
18 | ) {
19 | return Object.assign({}, l, {
20 | loaders: l.loaders
21 | .filter(e => !/awesome-typescript-loader.*/.test(e))
22 | .concat([
23 | 'awesome-typescript-loader?' +
24 | JSON.stringify({
25 | useBabel: true,
26 | babelOptions: {
27 | env: {
28 | test: {
29 | plugins: ['istanbul']
30 | }
31 | }
32 | },
33 | useCache: true,
34 | cacheDirectory:
35 | 'node_modules/.cache/at-loader'
36 | })
37 | ])
38 | });
39 | }
40 | return l;
41 | })
42 | })
43 | });
44 |
--------------------------------------------------------------------------------
/src/pages/FeedsList/view.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | PageState,
3 | PageParams
4 | } from '../types';
5 | import { VNode } from '@cycle/dom';
6 | import xs, { Stream } from 'xstream';
7 | import { pulse } from '../partials/_pulse';
8 |
9 | function prevLink(pageData: PageParams): VNode {
10 | return +pageData.page! > 1
11 | ? < prev
12 | : < prev ;
13 | }
14 |
15 | function nextLink(pageData: PageParams): VNode {
16 | return +pageData.page! < pageData.max
17 | ? next >
18 | : next >;
19 | }
20 |
21 | function currentPosition(pageData: PageParams): VNode {
22 | return {`${pageData.page} / ${pageData.max}`};
23 | }
24 |
25 | function pager(pageData: PageParams): VNode {
26 | return (
27 |
28 | {prevLink(pageData)}
29 | {currentPosition(pageData)}
30 | {nextLink(pageData)}
31 |
32 | );
33 | }
34 |
35 | function startOrder(pageData: PageParams): number {
36 | return ((+pageData.page! - 1) * 30) + 1;
37 | }
38 |
39 | function content(state: PageState, feedsDom: VNode): VNode {
40 | return (
41 |
42 |
{feedsDom}
43 | {/* this needs to be looked into */}
44 | {state.meta && pager(state.meta)}
45 |
46 | );
47 | }
48 |
49 | export function view(state$: Stream, feedsDom$: Stream): Stream {
50 | return xs.combine(state$, feedsDom$)
51 | .map(([state, feedsDom]) => {
52 | if (!state.isLoading) {
53 | return content(state, feedsDom);
54 | } else {
55 | return pulse();
56 | }
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/test/app.test.ts:
--------------------------------------------------------------------------------
1 | import { forall, assert, nat, Options } from 'jsverify';
2 | import { diagramArbitrary, withTime } from 'cyclejs-test-helpers';
3 | const htmlLooksLike = require('html-looks-like');
4 | const toHtml = require('snabbdom-to-html'); //snabbdom-to-html's typings are broken
5 |
6 | import xs, { Stream } from 'xstream';
7 | import { mockDOMSource, VNode } from '@cycle/dom';
8 | import { mockTimeSource } from '@cycle/time';
9 | import onionify from 'cycle-onionify';
10 |
11 | import { App } from '../src/app';
12 |
13 | const testOptions: Options = {
14 | tests: 10,
15 | size: 200
16 | };
17 |
18 | describe('app tests', () => {
19 | const expectedHTML = (count: number) => `
20 |
21 |
My Awesome Cycle.js app
22 | Counter: ${count}
23 |
24 |
25 |
26 | `;
27 |
28 | it('should interact correctly', () => {
29 | const property = forall(
30 | diagramArbitrary,
31 | diagramArbitrary,
32 | (addDiagram, subtractDiagram) =>
33 | withTime(Time => {
34 | const add$ = Time.diagram(addDiagram);
35 | const subtract$ = Time.diagram(subtractDiagram);
36 |
37 | const DOM = mockDOMSource({
38 | '.add': { click: add$ },
39 | '.subtract': { click: subtract$ }
40 | });
41 |
42 | const app = onionify(App)({ DOM } as any);
43 | const html$ = (app.DOM as Stream).map(toHtml);
44 |
45 | const expected$ = xs
46 | .merge(add$.mapTo(+1), subtract$.mapTo(-1))
47 | .fold((acc, curr) => acc + curr, 0)
48 | .map(expectedHTML);
49 |
50 | Time.assertEqual(html$, expected$, htmlLooksLike);
51 | })
52 | );
53 |
54 | return assert(property, testOptions);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cycle HN
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/CommentAtom.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sinks,
3 | Sources,
4 | Component
5 | } from '../interfaces';
6 | import { VNode } from '@cycle/dom';
7 | import isolate from '@cycle/isolate';
8 | import xs, { Stream } from 'xstream';
9 | import { StateSource } from 'cycle-onionify';
10 | import { CommentCollection } from './CommentCollection';
11 |
12 | export interface State {
13 | id: number;
14 | level: number;
15 | user: string;
16 | time: number;
17 | time_ago: string;
18 | content: string;
19 | comments: Array;
20 | }
21 |
22 | export type Reducer = (prev: State) => State;
23 | export type CommentSinks = Sinks & { onion: Stream };
24 | export type CommentSources = Sources & { onion: StateSource };
25 |
26 | function intent(sources: Sources): Stream {
27 | return sources.DOM.select('.comment-hide')
28 | .events('click')
29 | .mapTo(undefined)
30 | .fold((show: boolean) => !show, true);
31 | }
32 |
33 | function view(state$: Stream, commentChildren$: Stream, action$: Stream): Stream {
34 | return xs.combine(state$, commentChildren$, action$)
35 | .map(([comment, children, showComment]) =>
36 |
37 |
38 | {comment.user}
39 | {comment.time_ago}
40 | [{showComment ? '-' : '+'}]
41 |
42 | {showComment && }
45 | {(children && showComment) && }
48 |
49 | );
50 | }
51 |
52 | export const CommentAtom: Component = function(sources: CommentSources): CommentSinks {
53 | const action$ = intent(sources);
54 | const commentChildren = isolate(CommentCollection, 'comments')(sources);
55 |
56 | const vdom$ = view(sources.onion.state$, commentChildren.DOM, action$);
57 |
58 | const sinks = {
59 | DOM: vdom$,
60 | onion: xs.of((state) => state)
61 | };
62 |
63 | return sinks;
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/FeedAtom.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sinks,
3 | Sources,
4 | Component
5 | } from '../interfaces';
6 | import { VNode } from '@cycle/dom';
7 | import xs, { Stream } from 'xstream';
8 | import { StateSource } from 'cycle-onionify';
9 |
10 | export interface State {
11 | id: number;
12 | title: string;
13 | points: number;
14 | user: string;
15 | time: number;
16 | time_ago: string;
17 | comments_count: number;
18 | content?: string;
19 | type: string;
20 | url: string;
21 | domain: string;
22 | [data: string]: any;
23 | }
24 |
25 | export type Reducer = (prev: State) => State;
26 | export type FeedSinks = Sinks & { onion: Stream };
27 | export type FeedSources = Sources & { onion: StateSource };
28 |
29 | function url(feed: State): VNode {
30 | if (feed.url && feed.url.startsWith('http')) {
31 | return {feed.title};
32 | } else {
33 | return {feed.title};
34 | }
35 | }
36 |
37 | function domain(feed: State): any {
38 | return feed.domain && ({feed.domain});
39 | }
40 |
41 | function author(feed: State): any {
42 | return feed.type !== 'job'
43 | &&
44 | by
45 | {feed.user}
46 | ;
47 | }
48 |
49 | function points(feed: State): any {
50 | return feed.type !== 'job' && {feed.points} points ;
51 | }
52 |
53 | function commentsLink(feed: State): any {
54 | return feed.type !== 'job' && | {feed.comments_count} comments ;
55 | }
56 |
57 | function view(state$: Stream): Stream {
58 | return state$.map(feed =>
59 |
60 | {url(feed)}
61 | {domain(feed)}
62 |
63 | {points(feed)}
64 | {author(feed)}
65 | {feed.time_ago} {commentsLink(feed)}
66 |
67 |
68 | );
69 | }
70 |
71 | export const FeedAtom: Component = function(sources: FeedSources): FeedSinks {
72 | const vdom$ = view(sources.onion.state$);
73 |
74 | const sinks = {
75 | DOM: vdom$,
76 | onion: xs.of((state) => state)
77 | };
78 |
79 | return sinks;
80 | };
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cycle-hn",
3 | "author": "Usman Riaz",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "http://github.com/usm4n/cycle-hn.git"
8 | },
9 | "version": "0.1.0",
10 | "private": true,
11 | "devDependencies": {
12 | "@types/mocha": "^2.2.41",
13 | "@types/history": "^4.6.0",
14 | "@types/webpack-env": "^1.13.0",
15 | "@webpack-blocks/dev-server2": "^0.4.0",
16 | "@webpack-blocks/extract-text2": "^0.4.0",
17 | "@webpack-blocks/postcss": "^0.4.3",
18 | "@webpack-blocks/sass": "^0.4.1",
19 | "@webpack-blocks/tslint": "^0.4.0",
20 | "@webpack-blocks/typescript": "^0.4.1",
21 | "@webpack-blocks/webpack2": "^0.4.0",
22 | "autoprefixer": "^7.1.1",
23 | "babel-core": "^6.25.0",
24 | "babel-plugin-istanbul": "^4.1.4",
25 | "chalk": "^2.0.1",
26 | "clean-webpack-plugin": "^0.1.16",
27 | "copy-webpack-plugin": "^4.0.1",
28 | "cross-env": "^5.0.1",
29 | "cycle-restart": "^0.2.2",
30 | "cyclejs-test-helpers": "^1.3.0",
31 | "html-looks-like": "^1.0.3",
32 | "html-webpack-plugin": "^2.29.0",
33 | "ifdef-loader": "^1.1.1",
34 | "jsverify": "^0.8.2",
35 | "mocha": "^3.4.2",
36 | "mocha-webpack": "^0.7.0",
37 | "node-sass": "^4.5.3",
38 | "nyc": "^11.0.3",
39 | "rimraf": "^2.6.1",
40 | "snabbdom-pragma": "^2.4.0",
41 | "snabbdom-to-html": "^3.2.0",
42 | "typescript": "2.4.x",
43 | "uglifyjs-webpack-plugin": "^1.0.0-beta.3",
44 | "webpack": "^2.6.1",
45 | "webpack-dev-server": "^2.5.0",
46 | "webpack-node-externals": "^1.6.0",
47 | "sw-precache-webpack-plugin": "^0.11.4"
48 | },
49 | "dependencies": {
50 | "@cycle/dom": "^18.0.0",
51 | "@cycle/history": "^6.4.0",
52 | "@cycle/http": "^14.0.0",
53 | "@cycle/isolate": "^3.0.0",
54 | "@cycle/run": "^3.1.0",
55 | "@cycle/time": "^0.8.0",
56 | "cycle-onionify": "4.0.0",
57 | "cyclejs-utils": "^1.0.4",
58 | "switch-path": "^1.2.0",
59 | "xstream": "^10.9.0"
60 | },
61 | "scripts": {
62 | "start": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.js",
63 | "test": "cross-env NODE_ENV=test nyc mocha-webpack --timeout=100000 --colors --webpack-config configs/webpack.config.test.js test/**/*.test.*",
64 | "build": "cross-env NODE_ENV=production webpack --config configs/webpack.config.js",
65 | "clean": "rimraf build .tmp .nyc_output coverage"
66 | },
67 | "nyc": {
68 | "include": [
69 | "src"
70 | ],
71 | "reporter": [
72 | "html",
73 | "text-summary"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { Location } from 'history';
2 | import switchPath from 'switch-path';
3 | import xs, { Stream } from 'xstream';
4 | import isolate from '@cycle/isolate';
5 | import { PageState } from './pages/types';
6 | import FeedsList from './pages/FeedsList';
7 | import { StateSource } from 'cycle-onionify';
8 | import { extractSinks } from 'cyclejs-utils';
9 | import { Sources, Sinks } from './interfaces';
10 | import { VNode, DOMSource } from '@cycle/dom';
11 | import { Routes, MatchedRoute } from './routes';
12 |
13 | export const API_URL = 'https://hnpwa.com/api/v0';
14 |
15 | export type AppState = {
16 | page: PageState;
17 | };
18 |
19 | const defaultAppState: AppState = {
20 | page: {} as PageState
21 | };
22 |
23 | export type Reducer = (prev?: AppState) => AppState | undefined;
24 | export type AppSinks = Sinks & { onion: Stream };
25 | export type AppSources = Sources & { onion: StateSource};
26 |
27 | function navigation(pathname: string): VNode {
28 | return (
29 |
30 | top
31 | new
32 | show
33 | ask
34 | jobs
35 |
36 | );
37 | }
38 |
39 | function view(history$: Stream, vdom$: Stream): Stream {
40 | return xs.combine(history$, vdom$).map(([{pathname}, vdom]: [{pathname: string}, VNode]) =>
41 |
42 |
51 |
52 | {vdom}
53 |
54 |
57 |
58 | );
59 | }
60 |
61 | function initState(): Stream {
62 | const initReducer$ = xs.of(
63 | prevState => (prevState === undefined ? defaultAppState : prevState)
64 | );
65 |
66 | return initReducer$;
67 | }
68 |
69 | export function App(sources: AppSources): AppSinks {
70 | const history$: Stream = sources.History;
71 |
72 | const initState$ = initState();
73 |
74 | const pageSinks$ = history$.map((location: Location): MatchedRoute => {
75 | const {pathname} = location;
76 |
77 | return switchPath(pathname, Routes);
78 | }).map((route: MatchedRoute) => isolate(route.value, 'page')(sources));
79 |
80 | const pageSinks = extractSinks(pageSinks$, ['DOM', 'HTTP', 'onion']);
81 |
82 | const reducers$ = xs.merge(pageSinks.onion, initState$);
83 |
84 | const vdom$ = view(history$, pageSinks.DOM as Stream);
85 |
86 | return {
87 | DOM: vdom$,
88 | onion: reducers$,
89 | HTTP: pageSinks.HTTP
90 | };
91 | }
92 |
--------------------------------------------------------------------------------
/configs/webpack.config.js:
--------------------------------------------------------------------------------
1 | const {
2 | createConfig,
3 | defineConstants,
4 | env,
5 | entryPoint,
6 | setOutput,
7 | sourceMaps,
8 | addPlugins
9 | } = require('@webpack-blocks/webpack2');
10 | const devServer = require('@webpack-blocks/dev-server2');
11 | const postcss = require('@webpack-blocks/postcss');
12 | const sass = require('@webpack-blocks/sass');
13 | const typescript = require('@webpack-blocks/typescript');
14 | const tslint = require('@webpack-blocks/tslint');
15 | const extractText = require('@webpack-blocks/extract-text2');
16 | const autoprefixer = require('autoprefixer');
17 | const webpack = require('webpack');
18 | const HtmlWebpackPlugin = require('html-webpack-plugin');
19 | const CopyWebpackPlugin = require('copy-webpack-plugin');
20 | const CleanWebpackPlugin = require('clean-webpack-plugin');
21 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
22 | const SWPreCachePlugin = require('sw-precache-webpack-plugin');
23 |
24 | const path = require('path');
25 | const fs = require('fs');
26 |
27 | const preprocessor = production => ({
28 | PRODUCTION: production,
29 | DEVELOPMENT: !production
30 | });
31 |
32 | const ifdef = (opts, block) => context => {
33 | let conf = block(context);
34 | conf.module.loaders[0].loaders.push(
35 | `ifdef-loader?json=${JSON.stringify(opts)}`
36 | );
37 | return conf;
38 | };
39 |
40 | const tsIfDef = production =>
41 | ifdef(
42 | preprocessor(production),
43 | typescript({
44 | useCache: true,
45 | cacheDirectory: 'node_modules/.cache/at-loader'
46 | })
47 | );
48 |
49 | const appPath = (...names) => path.join(process.cwd(), ...names);
50 |
51 | const customConfig = fs.existsSync(appPath('webpack.config.js'))
52 | ? require(appPath('webpack.config.js'))
53 | : {};
54 |
55 | if (customConfig === undefined) {
56 | throw new Error(
57 | 'The 3.0 update is a breaking release, you need to upgrade manually. Please refer to https://github.com/cyclejs-community/create-cycle-app-flavors#migrating'
58 | );
59 | }
60 |
61 | module.exports = createConfig([
62 | () => customConfig, //Include user config
63 | tslint(),
64 | sass(),
65 | postcss([autoprefixer({ browsers: ['last 2 versions'] })]),
66 | defineConstants({
67 | 'process.env.NODE_ENV': process.env.NODE_ENV
68 | }),
69 | addPlugins([
70 | new HtmlWebpackPlugin({
71 | template: './index.ejs',
72 | inject: true,
73 | favicon: 'public/favicon.ico',
74 | hash: true
75 | }),
76 | new webpack.ProvidePlugin({
77 | Snabbdom: 'snabbdom-pragma'
78 | })
79 | ]),
80 | env('development', [
81 | tsIfDef(false),
82 | devServer(),
83 | sourceMaps(),
84 | addPlugins([new webpack.NamedModulesPlugin()])
85 | ]),
86 | env('production', [
87 | tsIfDef(true),
88 | extractText('[name].[chunkhash:8].css', 'text/x-sass'),
89 | addPlugins([
90 | new CleanWebpackPlugin([appPath('build')], {
91 | root: process.cwd()
92 | }),
93 | new CopyWebpackPlugin([{ from: 'public', to: 'public' }]),
94 | new UglifyJsPlugin(),
95 | new SWPreCachePlugin({
96 | minify: true,
97 | cacheId: 'cycle-hn',
98 | filename: 'service-worker.js',
99 | navigateFallback: 'index.html',
100 | dontCacheBustUrlsMatching:/\.\w{8}\./,
101 | staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/],
102 | })
103 | ])
104 | ]),
105 | env('test', [tsIfDef(true)])
106 | ]);
107 |
--------------------------------------------------------------------------------
/src/css/styles.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #fff;
3 | color: #202020;
4 | font-family : Roboto, "Helvetica Neue", "DejaVu Sans", "Fira Sans", "Droid Sans", sans-serif;
5 | font-size: 14px;
6 | margin: 0;
7 | overflow-y: scroll;
8 | }
9 | .main-wrapper {
10 | background-color: #f3f6fb;
11 | width: 100%;
12 | min-height: 75px;
13 | height: 100%;
14 | }
15 | .header-wrapper {
16 | color: #c6fc93;
17 | background-color: #24242d;
18 | font-size: 16px;
19 | height: 40px;
20 | line-height: 16px;
21 | overflow: hidden;
22 | padding: 4px;
23 | position: relative;
24 | .header-inner {
25 | width: 75%;
26 | margin: 0 auto;
27 | padding: 10px;
28 | vertical-align: middle;
29 | .logo {
30 | border: 1px #c6fc93 solid;
31 | margin-right: 15px;
32 | vertical-align: middle;
33 | width: 24px;
34 | }
35 | a {
36 | margin: 0 5px;
37 | color: #c6fc93;
38 | text-decoration: none;
39 | &.active {
40 | color: #f3f6fb;
41 | }
42 | }
43 | .home {
44 | font-weight: bold;
45 | margin-right: .5em;
46 | @media screen and (max-width: 600px) {
47 | display: none;
48 | }
49 | }
50 | @media screen and (max-width: 600px) {
51 | width: 100%;
52 | }
53 | }
54 | }
55 | .footer {
56 | color: #808080;
57 | background-color: #fff;
58 | margin: 0 auto;
59 | width: 75%;
60 | height: 30px;
61 | overflow: hidden;
62 | padding: 15px 0;
63 | text-align: center;
64 | .github {
65 | color: #606060;
66 | font-weight: bold;
67 | text-decoration: none;
68 | }
69 | @media screen and (max-width: 600px) {
70 | width: 100%;
71 | }
72 | p { margin: 0; }
73 | }
74 | .main-content {
75 | background-color: #fff;
76 | border-bottom: 2px #24242d solid;
77 | width: 75%;
78 | line-height: 1.5em;
79 | min-height: 85vh;
80 | margin: 0 auto;
81 | overflow: hidden;
82 | position: relative;
83 | word-wrap: break-word;
84 | a {
85 | color: #202020;
86 | text-decoration: none;
87 | &:hover {
88 | text-decoration: underline;
89 | }
90 | &:visited {
91 | color: #606060;
92 | }
93 | }
94 | @media screen and (max-width: 600px) {
95 | width: 100%;
96 | margin: 0;
97 | }
98 | }
99 | .feed-list {
100 | margin: 10px 0;
101 | padding-left: 35px;
102 | padding-right: 15px;
103 | }
104 | .feed-view {
105 | margin: 10px 0;
106 | padding: 0 20px;
107 | }
108 | .feed {
109 | color: #808080;
110 | font-size: 12px;
111 | margin: 10px 0;
112 | }
113 | .feed-title {
114 | font-size: 16px;
115 | }
116 | .feed-domain {
117 | margin-left: 10px;
118 | }
119 | .feed-author {
120 | font-weight: bold;
121 | }
122 | .feed-pager {
123 | text-align: center;
124 | padding: 20px 0;
125 | font-weight: bold;
126 | .disabled {
127 | color: #808080;
128 | }
129 | .pager-position {
130 | color: #808080;
131 | margin: 0 10px;
132 | font-weight: normal;
133 | }
134 | }
135 | .comment-list {
136 | list-style: none;
137 | padding: 0;
138 | margin: 20px 0;
139 | .reply {
140 | font-size: 12px;
141 | text-decoration: underline;
142 | }
143 | .comment-list {
144 | margin-left: 20px;
145 | margin-top: 0px;
146 | margin-bottom: 0px;
147 | }
148 | }
149 | .comment {
150 | padding: 8px 0px;
151 | }
152 | .comment-hide {
153 | color: #202002;
154 | cursor: pointer;
155 | font-size: 13.3333px;
156 | }
157 | .comment-header {
158 | font-size: 12px;
159 | color: #808080;
160 | margin-bottom: 8px;
161 | &.active {
162 | background: #c6fc93;
163 | }
164 | }
165 | .comment-author {
166 | color: #606060;
167 | font-weight: bold;
168 | }
169 | .pulse {
170 | position: absolute;
171 | top: 5%;
172 | left: 2%;
173 | .pulse-bar {
174 | background-color: #24242d;
175 | display: inline-block;
176 | width: 4px;
177 | height: 20px;
178 | margin: 0 2px;
179 | border-radius: 4px;
180 | animation: pulse 1s ease-in-out infinite;
181 | &:nth-child(1) {
182 | animation-delay: 0;
183 | }
184 | &:nth-child(2) {
185 | animation-delay: 0.09s;
186 | }
187 | &:nth-child(3) {
188 | animation-delay: .18s;
189 | }
190 | &:nth-child(4) {
191 | animation-delay: .27s;
192 | }
193 | }
194 | }
195 | @keyframes pulse {
196 | 0% {
197 | background-color: #c6fc93;
198 | transform: scale(1);
199 | }
200 | 20% {
201 | transform: scale(1, 2.2);
202 | }
203 | 40% {
204 | transform: scale(1);
205 | }
206 | }
--------------------------------------------------------------------------------