├── .babelrc ├── .gitignore ├── src ├── client │ ├── assets │ │ ├── images │ │ │ ├── avatar.png │ │ │ ├── bg-header.png │ │ │ ├── fg-header.png │ │ │ └── logo_eleven_lab.svg │ │ ├── fonts │ │ │ ├── din-light-webfont.woff │ │ │ ├── din_black-webfont.woff │ │ │ ├── din-light-webfont.woff2 │ │ │ └── din_black-webfont.woff2 │ │ └── scss │ │ │ ├── objects │ │ │ ├── _newsletter.scss │ │ │ ├── _calendar.scss │ │ │ ├── _splash.scss │ │ │ ├── _avatar.scss │ │ │ ├── _read-also.scss │ │ │ ├── _page-heading.scss │ │ │ ├── _form.scss │ │ │ ├── _buttons.scss │ │ │ ├── _layout.scss │ │ │ ├── _meta.scss │ │ │ └── _search-bar.scss │ │ │ ├── includes │ │ │ ├── _footer.scss │ │ │ ├── _author.scss │ │ │ ├── _md-content.scss │ │ │ └── _header.scss │ │ │ ├── settings │ │ │ ├── _fonts.scss │ │ │ └── _variables.scss │ │ │ ├── main.scss │ │ │ ├── layouts │ │ │ ├── _index.scss │ │ │ └── _posts.scss │ │ │ ├── base │ │ │ ├── _utility.scss │ │ │ └── _global.scss │ │ │ └── external │ │ │ ├── _syntax.scss │ │ │ └── _reset.scss │ ├── constants.ts │ ├── routes │ │ ├── NotFound.tsx │ │ ├── Status.tsx │ │ └── index.ts │ ├── services │ │ ├── posts.ts │ │ └── Request.ts │ ├── store │ │ ├── prod.ts │ │ └── dev.ts │ ├── containers │ │ ├── App.tsx │ │ ├── Home.tsx │ │ └── Scheduler.tsx │ ├── components │ │ ├── Header.tsx │ │ ├── FormGroup.tsx │ │ └── Post.tsx │ ├── reducers │ │ └── index.ts │ ├── index.dev.tsx │ ├── index.prod.tsx │ ├── epics │ │ └── index.ts │ └── actions │ │ └── index.ts └── server │ ├── api │ ├── index.ts │ └── posts.json │ ├── middlewares │ ├── 404.ts │ └── 500.ts │ ├── templates │ ├── error.ejs │ └── index.ejs │ ├── routes │ └── index.tsx │ └── index.ts ├── app.yaml ├── definitions ├── global.d.ts ├── json-server.d.ts ├── common.d.ts └── react-infinite-calendar.d.ts ├── tsconfig-server.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["dynamic-import-webpack"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | public 5 | coverage 6 | -------------------------------------------------------------------------------- /src/client/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/images/avatar.png -------------------------------------------------------------------------------- /src/client/assets/images/bg-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/images/bg-header.png -------------------------------------------------------------------------------- /src/client/assets/images/fg-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/images/fg-header.png -------------------------------------------------------------------------------- /src/client/assets/fonts/din-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/fonts/din-light-webfont.woff -------------------------------------------------------------------------------- /src/client/assets/fonts/din_black-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/fonts/din_black-webfont.woff -------------------------------------------------------------------------------- /src/client/assets/fonts/din-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/fonts/din-light-webfont.woff2 -------------------------------------------------------------------------------- /src/client/assets/fonts/din_black-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfakamal/the-wilson-post/HEAD/src/client/assets/fonts/din_black-webfont.woff2 -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_newsletter.scss: -------------------------------------------------------------------------------- 1 | .newsletter { 2 | background: $brand-yellow; 3 | text-align: center; 4 | 5 | &-link { 6 | @extend %h2; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/constants.ts: -------------------------------------------------------------------------------- 1 | import { State } from 'common'; 2 | 3 | export const API_ROOT = 'http://localhost:8080'; 4 | 5 | export const INITIAL_STATE: State = { 6 | posts: [], 7 | }; 8 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs 2 | env: flex 3 | health_check: 4 | enable_health_check: true 5 | check_interval_sec: 5 6 | timeout_sec: 4 7 | unhealthy_threshold: 2 8 | healthy_threshold: 2 9 | -------------------------------------------------------------------------------- /src/client/assets/scss/includes/_footer.scss: -------------------------------------------------------------------------------- 1 | .site-footer { 2 | @extend %padding-regular; 3 | color: lighten($brand-black, 30%); 4 | display: inline-block; 5 | font-size: .9rem; 6 | text-align: center; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /src/client/routes/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Status from './Status'; 4 | 5 | export default (): JSX.Element => ( 6 | 7 |
Not found
8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/server/api/index.ts: -------------------------------------------------------------------------------- 1 | import { create, router } from 'json-server'; 2 | import * as path from 'path'; 3 | 4 | const server = create(); 5 | const apiEndpoints = router(path.join(__dirname, 'posts.json')); 6 | 7 | server.use(apiEndpoints); 8 | 9 | export default server; 10 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_calendar.scss: -------------------------------------------------------------------------------- 1 | .Cal__Container__root { 2 | border: 1px solid #ddd; 3 | 4 | ul { 5 | line-height: inherit; 6 | } 7 | 8 | .Cal__Header__wrapper.Cal__Header__blank, 9 | .Cal__Header__dateWrapper { 10 | color: inherit; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_splash.scss: -------------------------------------------------------------------------------- 1 | .splash { 2 | background-image: url('../images/bg-header.png'); 3 | 4 | .title { 5 | font-weight: bold; 6 | font-size: $font-size-extra-large; 7 | position: relative; 8 | text-align: right; 9 | text-transform: uppercase; 10 | top: -2em; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_avatar.scss: -------------------------------------------------------------------------------- 1 | .avatar { 2 | border-radius: 50%; 3 | height: 100px; 4 | margin: 0 auto; 5 | overflow: hidden; 6 | position: relative; 7 | width: 100px; 8 | 9 | &-img { 10 | height: auto; 11 | left: 0; 12 | position: absolute; 13 | top: 0; 14 | width: 100%; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_read-also.scss: -------------------------------------------------------------------------------- 1 | .read-also { 2 | &-list { 3 | list-style: none; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | &-title { 9 | @extend %h3; 10 | margin-bottom: 1rem; 11 | } 12 | 13 | &-item { 14 | padding: .8rem 0; 15 | } 16 | 17 | &-link { 18 | @extend %h4; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /definitions/global.d.ts: -------------------------------------------------------------------------------- 1 | import { State } from 'common'; 2 | import * as moment from 'moment'; 3 | 4 | interface ExtendedWindow extends Window { 5 | __REDUX_DEVTOOLS_EXTENSION__: any; 6 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 7 | __INITIAL_STATE__: State; 8 | moment: moment.Moment; 9 | } 10 | 11 | export interface Error { 12 | status?: number; 13 | message?: any; 14 | } 15 | -------------------------------------------------------------------------------- /src/server/middlewares/404.ts: -------------------------------------------------------------------------------- 1 | import * as Express from 'express'; 2 | 3 | /** 4 | * Catch 404 and forward to error handler 5 | */ 6 | // tslint:disable-next-line:variable-name 7 | export default (_req: Express.Request, res: Express.Response, next: Express.NextFunction) => { 8 | const err: any = new Error('Not Found'); 9 | err.status = 404; 10 | res.status(404); 11 | next(err); 12 | }; 13 | -------------------------------------------------------------------------------- /src/client/assets/scss/includes/_author.scss: -------------------------------------------------------------------------------- 1 | .author-social { 2 | ul { 3 | line-height: 1.5; 4 | list-style: none; 5 | margin: 0; 6 | padding: 0; 7 | text-align: center; 8 | } 9 | 10 | li { 11 | display: inline; 12 | margin-right: .4rem; 13 | } 14 | 15 | @media (max-width: 1100px) { 16 | ul { 17 | display: inline-block; 18 | width: 100%; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_page-heading.scss: -------------------------------------------------------------------------------- 1 | .page-heading { 2 | background: $brand-yellow; 3 | 4 | @media (min-width: 480px) { 5 | padding: 60px 0; 6 | } 7 | 8 | @media (max-width: 480px) { 9 | padding: 30px 0; 10 | } 11 | 12 | &-title { 13 | @extend %h1; 14 | margin: 15px 0; 15 | } 16 | 17 | &-reading-time { 18 | font-size: 1.2rem; 19 | margin-top: 5px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /definitions/json-server.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'json-server' { 2 | import * as Express from 'express'; 3 | 4 | export function defaults(opts: any): Express.RequestHandler[]; 5 | export function router(source: any, ...args: any[]): Express.Router; 6 | export function create(): Express.Application; 7 | export function rewriter(routes: any): Express.Router; 8 | export const bodyParser: Express.RequestHandler[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_form.scss: -------------------------------------------------------------------------------- 1 | .form { 2 | input, textarea { 3 | width: 100%; 4 | border: 1px solid #ccc; 5 | } 6 | 7 | .form-group { 8 | margin-top: 20px; 9 | } 10 | 11 | .form-buttons { 12 | text-align: right; 13 | margin-top: 20px; 14 | 15 | button { 16 | @extend %h3; 17 | border: 1px solid rgba(0,0,0,0.1); 18 | outline: none; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/routes/Status.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, RouteComponentProps } from 'react-router-dom'; 3 | 4 | interface Props { 5 | status?: number; 6 | children?: React.ReactNode; 7 | } 8 | 9 | export default ({ status, children }: Props) => ( 10 | ) => { 11 | if (staticContext) { 12 | staticContext.status = status; 13 | } 14 | 15 | return children; 16 | }} /> 17 | ); 18 | -------------------------------------------------------------------------------- /src/client/assets/scss/settings/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'din'; 3 | src: url('../fonts/din-light-webfont.woff2') format('woff2'), url('../fonts/din-light-webfont.woff') format('woff'); 4 | font-weight: lighter; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'din'; 10 | src: url('../fonts/din_black-webfont.woff2') format('woff2'), url('../fonts/din_black-webfont.woff') format('woff'); 11 | font-weight: 800; 12 | font-style: normal; 13 | } 14 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_buttons.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | color: $brand-black; 3 | display: inline-block; 4 | font-family: $font-family-headings; 5 | // font-size: 1.1rem; 6 | font-weight: lighter; 7 | letter-spacing: .05em; 8 | text-decoration: underline; 9 | text-transform: uppercase; 10 | 11 | &:hover { 12 | background: $brand-yellow; 13 | text-decoration: none; 14 | } 15 | 16 | &:visited, 17 | &:visited:hover { 18 | color: $button-hover; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/services/posts.ts: -------------------------------------------------------------------------------- 1 | import { Post } from 'common'; 2 | import * as urlJoin from 'url-join'; 3 | 4 | import { API_ROOT } from '../constants'; 5 | import Request from './Request'; 6 | 7 | export const getPosts = async () => ( 8 | await Request.get(urlJoin(API_ROOT, 'api', 'posts')) 9 | ); 10 | 11 | export const addPost = async (post: Post) => { 12 | const payload = { post }; 13 | 14 | const url = urlJoin(API_ROOT, 'posts'); 15 | return await Request.post(url, payload); 16 | }; 17 | -------------------------------------------------------------------------------- /src/client/store/prod.ts: -------------------------------------------------------------------------------- 1 | import { State } from 'common'; 2 | import { 3 | applyMiddleware, 4 | compose, 5 | createStore, 6 | GenericStoreEnhancer, 7 | } from 'redux'; 8 | import { createEpicMiddleware } from 'redux-observable'; 9 | 10 | import rootEpic from '../epics'; 11 | import reducer from '../reducers'; 12 | 13 | const epicMiddleware = createEpicMiddleware(rootEpic); 14 | const enhancer: GenericStoreEnhancer = compose(applyMiddleware(epicMiddleware)); 15 | 16 | export default (initialState: State) => 17 | createStore(reducer, initialState, enhancer); 18 | -------------------------------------------------------------------------------- /src/client/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-dates/initialize'; 2 | 3 | import * as React from 'react'; 4 | import { renderRoutes, RouteConfig } from 'react-router-config'; 5 | 6 | import Header from '../components/Header'; 7 | 8 | interface Props { 9 | route?: RouteConfig; 10 | } 11 | 12 | export default class App extends React.Component { 13 | render() { 14 | const { route } = this.props; 15 | 16 | return ( 17 |
18 |
19 | {route && renderRoutes(route.routes)} 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/routes/index.ts: -------------------------------------------------------------------------------- 1 | import App from '../containers/App'; 2 | import Home from '../containers/Home'; 3 | import Scheduler from '../containers/Scheduler'; 4 | import NotFound from './NotFound'; 5 | 6 | export default [ 7 | { 8 | component: App, 9 | routes: [ 10 | { 11 | path: '/', 12 | exact: true, 13 | component: Home, 14 | }, 15 | { 16 | path: '/schedule-post', 17 | exact: true, 18 | component: Scheduler, 19 | }, 20 | { 21 | path: '*', 22 | component: NotFound, 23 | }, 24 | ], 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_layout.scss: -------------------------------------------------------------------------------- 1 | .slice { 2 | border-bottom: 1px solid $border-color; 3 | 4 | @media (min-width: 480px) { 5 | padding-bottom: 30px; 6 | padding-top: 30px; 7 | } 8 | 9 | @media (max-width: 480px) { 10 | padding-bottom: 25px; 11 | padding-top: 25px; 12 | } 13 | } 14 | 15 | .container { 16 | margin: 0 auto; 17 | max-width: $max-width-layout; 18 | 19 | @media (min-width: 480px) { 20 | padding-left: 45px; 21 | padding-right: 45px; 22 | } 23 | 24 | @media (max-width: 480px) { 25 | padding-left: 25px; 26 | padding-right: 25px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/middlewares/500.ts: -------------------------------------------------------------------------------- 1 | import * as Express from 'express'; 2 | 3 | import { Error } from '../../../definitions/global'; 4 | 5 | /** 6 | * error handler 7 | */ 8 | // tslint:disable:max-line-length 9 | // tslint:disable-next-line:variable-name 10 | export default (env: string) => (err: Error, _req: Express.Request, res: Express.Response, _next: Express.NextFunction) => { 11 | res.status(err.status || 500); 12 | res.render('error', { 13 | message: err.message, 14 | // for production mode, no stacktraces leaked to user. 15 | error: env === 'development' ? err : {}, 16 | status: err.status, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/server/templates/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The Wilson Post 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 |

<%= message %>

17 |

<%= error.status %>

18 |
<%= error.stack %>
19 | 20 | 21 | -------------------------------------------------------------------------------- /definitions/common.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'common' { 2 | import { Reducer } from 'redux'; 3 | 4 | interface Action { 5 | type: T; 6 | } 7 | 8 | interface Dict { 9 | [key: string]: T; 10 | } 11 | 12 | interface ReducersMapObject { 13 | [key: string]: Reducer; 14 | } 15 | 16 | interface State { 17 | posts?: Post[]; 18 | selectedPost?: Post; 19 | } 20 | 21 | interface Author { 22 | id: number; 23 | firstname: string; 24 | lastname: string; 25 | email: string; 26 | } 27 | 28 | interface Post { 29 | id: number; 30 | title: string; 31 | author: string; 32 | description: string; 33 | date: number; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "outDir": "./dist", 5 | "allowJs": true, 6 | "allowUnreachableCode": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "lib": [ 9 | "dom", 10 | "es2015", 11 | "es2016", 12 | "es2017" 13 | ], 14 | "module": "commonjs", 15 | "isolatedModules": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "skipDefaultLibCheck": true, 21 | "sourceMap": false, 22 | "strictNullChecks": true, 23 | "target": "es5" 24 | }, 25 | "include": [ 26 | "./definitions/**/*", 27 | "./src/server/**/*" 28 | ] 29 | } -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_meta.scss: -------------------------------------------------------------------------------- 1 | .meta { 2 | position: relative; 3 | 4 | @media (min-width: 480px) { 5 | font-size: 1.5rem; 6 | } 7 | 8 | @media (max-width: 480px) { 9 | font-size: 1.3rem; 10 | } 11 | 12 | &-content { 13 | display: inline-block; 14 | font-family: $font-family-headings; 15 | font-weight: lighter; 16 | letter-spacing: .05em; 17 | margin: 0; 18 | position: relative; 19 | text-transform: uppercase; 20 | z-index: 1; 21 | } 22 | 23 | &::before { 24 | background-color: $brand-yellow; 25 | bottom: -6px; 26 | content: ''; 27 | display: block; 28 | height: 1.1em; 29 | left: -1%; 30 | position: absolute; 31 | width: 108%; 32 | z-index: 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "allowJs": true, 5 | "allowUnreachableCode": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": [ 8 | "es2015", 9 | "es2016", 10 | "es2017", 11 | "dom" 12 | ], 13 | "allowSyntheticDefaultImports": true, 14 | "moduleResolution": "node", 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "module": "commonjs", 18 | "noEmitOnError": true, 19 | "noImplicitAny": true, 20 | "skipLibCheck": true, 21 | "sourceMap": true, 22 | "strictNullChecks": true, 23 | "target": "es5", 24 | "outDir": "dist" 25 | }, 26 | "include": [ 27 | "./definitions/**/*", 28 | "./src/client/**/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/server/templates/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Wilson Post 6 | 7 | 10 | 11 | 12 | 13 | 14 |
<%- content %>
15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/client/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default class Header extends React.Component { 5 | shouldComponentUpdate() { 6 | return false; 7 | } 8 | 9 | render() { 10 | return ( 11 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/client/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | // Settings 2 | @import 'settings/fonts'; 3 | @import 'settings/variables'; 4 | 5 | // External 6 | @import 'external/reset'; 7 | @import 'external/syntax'; 8 | 9 | // Base 10 | @import 'base/global'; 11 | @import 'base/utility'; 12 | 13 | // objects 14 | @import 'objects/layout'; 15 | @import 'objects/meta'; 16 | @import 'objects/buttons'; 17 | @import 'objects/page-heading'; 18 | @import 'objects/read-also'; 19 | @import 'objects/search-bar'; 20 | @import 'objects/avatar'; 21 | @import 'objects/newsletter'; 22 | @import 'objects/splash'; 23 | @import 'objects/form'; 24 | @import 'objects/calendar'; 25 | 26 | // Posts 27 | @import 'layouts/posts'; 28 | @import 'layouts/index'; 29 | 30 | // Partials 31 | @import 'includes/header'; 32 | @import 'includes/footer'; 33 | @import 'includes/md-content'; 34 | @import 'includes/author'; 35 | -------------------------------------------------------------------------------- /src/client/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { Action, ReducersMapObject } from 'common'; 2 | 3 | import { 4 | POSTS_SUCCESS, 5 | PostsSuccessAction, 6 | SCHEDULE_POST_SUCCESS, 7 | SchedulePostSuccessAction, 8 | } from '../actions'; 9 | import { INITIAL_STATE } from '../constants'; 10 | 11 | const handleActions = (cases: ReducersMapObject) => ( 12 | (state = INITIAL_STATE, action: Action) => ( 13 | (!action || !cases[action.type]) ? state : cases[action.type](state, action) 14 | ) 15 | ); 16 | 17 | export default handleActions({ 18 | [POSTS_SUCCESS]: (state, { posts }: PostsSuccessAction) => ({ 19 | ...state, 20 | posts, 21 | }), 22 | 23 | [SCHEDULE_POST_SUCCESS]: (state, { post }: SchedulePostSuccessAction) => ({ 24 | ...state, 25 | posts: [ 26 | ...(state.posts || []), 27 | post, 28 | ], 29 | }), 30 | }); 31 | -------------------------------------------------------------------------------- /src/client/index.dev.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | import { ExtendedWindow } from '../../definitions/global'; 8 | import { INITIAL_STATE } from './constants'; 9 | import routes from './routes'; 10 | import createStore from './store/dev'; 11 | 12 | import 'react-infinite-calendar/styles.css'; 13 | import './assets/scss/main.scss'; 14 | 15 | const win: ExtendedWindow = window as ExtendedWindow; 16 | const state = win && win.__INITIAL_STATE__ ? win.__INITIAL_STATE__ : INITIAL_STATE; 17 | const store = createStore(state); 18 | 19 | hydrate( 20 | 21 | 22 | {renderRoutes(routes)} 23 | 24 | , 25 | document.getElementById('root'), 26 | ); 27 | -------------------------------------------------------------------------------- /src/client/index.prod.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | import { ExtendedWindow } from '../../definitions/global'; 8 | import { INITIAL_STATE } from './constants'; 9 | import routes from './routes'; 10 | import createStore from './store/prod'; 11 | 12 | import 'react-infinite-calendar/styles.css'; 13 | import './assets/scss/main.scss'; 14 | 15 | const win: ExtendedWindow = window as ExtendedWindow; 16 | const state = win && win.__INITIAL_STATE__ ? win.__INITIAL_STATE__ : INITIAL_STATE; 17 | const store = createStore(state); 18 | 19 | hydrate( 20 | 21 | 22 | {renderRoutes(routes)} 23 | 24 | , 25 | document.getElementById('root'), 26 | ); 27 | -------------------------------------------------------------------------------- /src/client/components/FormGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface Props { 4 | field: string; 5 | label: string; 6 | value?: string; 7 | onChange?: (event: React.SyntheticEvent) => void; 8 | children?: React.ReactElement; 9 | style?: React.CSSProperties; 10 | } 11 | 12 | const FormGroup = ({ field, label, value, onChange, style, children }: Props): JSX.Element => { 13 | const props: Pick = {}; 14 | 15 | if (value) { 16 | props.value = value; 17 | } 18 | 19 | if (onChange) { 20 | props.onChange = onChange; 21 | } 22 | 23 | if (style) { 24 | props.style = style; 25 | } 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 | {children ? React.cloneElement(children, props) : ( 33 | 34 | )} 35 |
36 | ); 37 | }; 38 | 39 | export default FormGroup; 40 | -------------------------------------------------------------------------------- /src/client/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Post, State } from 'common'; 2 | import * as React from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import PostComponent from '../components/Post'; 6 | 7 | interface Props { 8 | posts?: Post[]; 9 | } 10 | 11 | const mapStateToProps = (state: State): State => ({ 12 | posts: state.posts, 13 | }); 14 | 15 | type AllProps = Readonly; 16 | 17 | class Home extends React.Component { 18 | constructor(props: AllProps) { 19 | super(props); 20 | 21 | this.renderPost = this.renderPost.bind(this); 22 | } 23 | 24 | renderPost(post: Post) { 25 | return ( 26 | 27 | ); 28 | } 29 | 30 | render() { 31 | const { posts = [] } = this.props; 32 | 33 | return ( 34 |
35 |
36 | {posts.map(this.renderPost)} 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default connect(mapStateToProps, {})(Home); 44 | -------------------------------------------------------------------------------- /src/client/services/Request.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | 3 | const methods = ['get', 'post', 'put', 'patch', 'delete']; 4 | 5 | interface Request { 6 | [key: string]: (endpoint: string, requestBody?: any) => Promise; 7 | } 8 | 9 | export default methods.reduce((Request: Request, method) => ({ 10 | ...Request, 11 | [method]: async (endpoint: string, requestBody?: any) => { 12 | const response: Response = await fetch(endpoint, { 13 | method, 14 | ...(!requestBody ? {} : { 15 | body: JSON.stringify(requestBody), 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | }, 19 | }), 20 | }); 21 | 22 | let results: any = await response.text(); 23 | 24 | if (response.headers) { 25 | const contentType = response.headers.get('content-type'); 26 | 27 | if (contentType && contentType.match(/application\/json/)) { 28 | results = JSON.parse(results); 29 | } 30 | } 31 | 32 | if (!response.ok) { 33 | throw new Error(results); 34 | } 35 | 36 | return results; 37 | }, 38 | }), {} as Request); 39 | -------------------------------------------------------------------------------- /src/server/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Express from 'express'; 2 | import * as React from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | import { Provider } from 'react-redux'; 5 | import { renderRoutes } from 'react-router-config'; 6 | import { StaticRouter } from 'react-router-dom'; 7 | 8 | import routes from '../../client/routes'; 9 | import { getPosts } from '../../client/services/posts'; 10 | import createStore from '../../client/store/dev'; 11 | 12 | interface StaticRouterContext { 13 | status?: number; 14 | } 15 | 16 | export default async (req: Express.Request, res: Express.Response) => { 17 | const posts = await getPosts(); 18 | const store = createStore({ posts }); 19 | const context: StaticRouterContext = {}; 20 | 21 | const content = renderToString( 22 | 23 | 24 | {renderRoutes(routes)} 25 | 26 | , 27 | ); 28 | 29 | if (context.status === 404) { 30 | res.status(404); 31 | } 32 | 33 | res.render('index', { content, data: store.getState() }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/client/store/dev.ts: -------------------------------------------------------------------------------- 1 | import { State } from 'common'; 2 | import { 3 | applyMiddleware, 4 | compose, 5 | createStore, 6 | GenericStoreEnhancer, 7 | } from 'redux'; 8 | import { createLogger } from 'redux-logger'; 9 | import { createEpicMiddleware } from 'redux-observable'; 10 | 11 | import { ExtendedWindow } from '../../../definitions/global'; 12 | import rootEpic from '../epics'; 13 | import reducer from '../reducers'; 14 | 15 | let enhancer: GenericStoreEnhancer; 16 | const epicMiddleware = createEpicMiddleware(rootEpic); 17 | 18 | const hasDevtools = Boolean( 19 | typeof window !== 'undefined' && 20 | (window as ExtendedWindow).__REDUX_DEVTOOLS_EXTENSION__ && 21 | (window as ExtendedWindow).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__, 22 | ); 23 | 24 | if (hasDevtools) { 25 | enhancer = (window as ExtendedWindow).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__( 26 | compose(applyMiddleware(epicMiddleware, createLogger())), 27 | ); 28 | } else { 29 | enhancer = compose(applyMiddleware(epicMiddleware, createLogger())); 30 | } 31 | 32 | export default (initialState: State) => ( 33 | createStore(reducer, initialState, enhancer) 34 | ); 35 | -------------------------------------------------------------------------------- /src/client/assets/scss/settings/_variables.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | $font-family-main: Helvetica, Arial, sans-serif; 3 | $font-family-headings: 'din', Helvetica, Arial, sans-serif; 4 | $font-size: 1.9rem; 5 | $font-size-mobile: 1.6rem; 6 | $font-size-extra-large: 48px; 7 | 8 | // Padding 9 | $padding-large: 20%; 10 | $padding-small: 5%; 11 | $padding-x-small: 3%; 12 | 13 | // Button 14 | $button-hover: #999; 15 | 16 | // Brand colours 17 | $brand-color: #fff; 18 | $background-color: #fff; 19 | $border-color: rgba(0, 0, 0, .1); // rgba recommended if using feature images 20 | 21 | // Typography colours 22 | $link-color: #0096fb; 23 | $selection-color: #d4d4d4; // visible when highlighting text 24 | 25 | // Header colours 26 | $header-link-color: #383838; 27 | 28 | // Feature image for articles 29 | $feature-image-text-color: #fff; 30 | $feature-image-size: cover; // options include "cover", "contain", "auto" 31 | 32 | // Header description box 33 | $header-desc-background-color: #fbe300; 34 | $header-desc-text-color: #fff; 35 | 36 | // brand colors 37 | $brand-yellow: #fbe300; 38 | $brand-black: #323334; 39 | 40 | // layout 41 | $max-width-layout: 960px; 42 | -------------------------------------------------------------------------------- /src/client/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from 'common'; 2 | import * as React from 'react'; 3 | 4 | interface Props { 5 | post: Post; 6 | } 7 | 8 | const dateOptions = { 9 | year: 'numeric', 10 | month: 'short', 11 | day: 'numeric', 12 | }; 13 | 14 | const timeOptions = { 15 | hour: '2-digit', 16 | minute: '2-digit', 17 | }; 18 | 19 | export default class extends React.Component { 20 | static displayName = 'Post'; 21 | 22 | render() { 23 | const { post = {} as Post } = this.props; 24 | const date = new Date(post.date); 25 | 26 | return ( 27 |
28 |
29 |

30 | {post.title} 31 |

32 | 33 | 41 | 42 |

{post.description}

43 |
44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as express from 'express'; 3 | import * as morgan from 'morgan'; 4 | import * as path from 'path'; 5 | import * as serveStatic from 'serve-static'; 6 | 7 | import api from './api'; 8 | import handler404 from './middlewares/404'; 9 | import handler500 from './middlewares/500'; 10 | import routes from './routes'; 11 | 12 | const publicPath = path.join(__dirname, '..', '..', 'public'); 13 | const templatesPath = path.join(__dirname, 'templates'); 14 | const port = 8080; 15 | const app = express(); 16 | 17 | app.set('views', templatesPath); 18 | app.set('view engine', 'ejs'); 19 | app.use(morgan('combined')); 20 | app.use(bodyParser.urlencoded({ extended: true })); 21 | app.use(bodyParser.json()); 22 | 23 | app.use('/static', serveStatic(publicPath)); 24 | app.use('/api', api); 25 | app.use('/', routes); 26 | 27 | // error handlers 28 | app.use(handler500(app.get('env'))); 29 | app.use(handler404); 30 | 31 | app.listen(port, (error: any) => { 32 | if (error) { 33 | console.error(JSON.stringify(error, null, 2)); 34 | } else { 35 | // tslint:disable-next-line:max-line-length 36 | console.info('==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/client/assets/scss/layouts/_index.scss: -------------------------------------------------------------------------------- 1 | // Header text feature 2 | .call-out { 3 | @extend %padding-regular; 4 | background-color: $header-desc-background-color; 5 | background-size: cover; 6 | color: $header-desc-text-color; 7 | display: inline-block; 8 | font-size: 1.2rem; 9 | text-align: center; 10 | width: 100%; 11 | 12 | p:last-child { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | // Post listing 18 | .posts { 19 | &-teaser { 20 | background-size: $feature-image-size; 21 | display: inline-block; 22 | margin-bottom: 0; 23 | width: 100%; 24 | 25 | p a:hover { 26 | @extend %link-hover; 27 | } 28 | } 29 | 30 | &-title { 31 | @extend %h2; 32 | margin: 0 0 5px; 33 | 34 | a { 35 | color: $brand-black; 36 | text-decoration: none; 37 | } 38 | } 39 | 40 | &-date { 41 | display: inline-block; 42 | margin-bottom: 1.5rem; 43 | } 44 | 45 | &-button { 46 | margin-top: 1.5rem; 47 | } 48 | 49 | .excerpt { 50 | margin-bottom: 1.5rem; 51 | margin-top: 1rem; 52 | } 53 | } 54 | // Pagination 55 | .pagination { 56 | padding: $padding-small $padding-large 0; 57 | text-align: center; 58 | 59 | .button { 60 | margin: 0 1.5rem; 61 | 62 | i { 63 | vertical-align: middle; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/client/assets/scss/includes/_md-content.scss: -------------------------------------------------------------------------------- 1 | .post-content { 2 | @media (min-width: 480px) { 3 | padding-top: 30px; 4 | } 5 | 6 | @media (max-width: 480px) { 7 | padding-top: 10px; 8 | } 9 | 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6 { 16 | font-family: $font-family-headings; 17 | line-height: 1.1; 18 | 19 | a { 20 | color: $brand-black; 21 | } 22 | 23 | &:first-child { 24 | margin-top: 0; 25 | } 26 | } 27 | 28 | h1 { 29 | @extend %h1; 30 | 31 | @media (min-width: 480px) { 32 | margin: 2em 0 .7em -15px; 33 | } 34 | 35 | @media (max-width: 480px) { 36 | margin: 2em 0 .7em; 37 | } 38 | } 39 | 40 | h2 { 41 | @extend %h2; 42 | 43 | @media (min-width: 480px) { 44 | margin: 2em 0 .7em -15px; 45 | } 46 | 47 | @media (max-width: 480px) { 48 | margin: 2em 0 .7em; 49 | } 50 | } 51 | 52 | h3 { 53 | @extend %h3; 54 | margin: .7em 0; 55 | } 56 | 57 | h4 { 58 | @extend %h4; 59 | margin: .7em 0; 60 | } 61 | 62 | blockquote { 63 | border-left: 5px solid $brand-yellow; 64 | font-style: italic; 65 | margin: 3rem 0; 66 | opacity: .8; 67 | padding-left: 2rem; 68 | } 69 | 70 | pre { 71 | margin: 3rem 0; 72 | padding: 15px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/client/assets/scss/layouts/_posts.scss: -------------------------------------------------------------------------------- 1 | .comments { 2 | @extend %padding-regular; 3 | border-bottom: 1px solid $border-color; 4 | float: left; 5 | width: 100%; 6 | } 7 | 8 | article { 9 | header { 10 | text-align: center; 11 | } 12 | 13 | a:hover { 14 | @extend %link-hover; 15 | } 16 | 17 | .footnotes { 18 | font-size: .9rem; 19 | } 20 | } 21 | 22 | .feature-image { 23 | padding: 0; 24 | 25 | .post-link { 26 | color: $feature-image-text-color; 27 | } 28 | 29 | header { 30 | background-position-x: center; 31 | background-size: $feature-image-size; 32 | color: $feature-image-text-color; 33 | margin-bottom: 0; 34 | padding: $padding-large / 2.5 $padding-large; 35 | 36 | .meta::before { 37 | background-color: transparent; 38 | } 39 | 40 | .author-link { 41 | color: $brand-color; 42 | } 43 | } 44 | } 45 | 46 | // Post navigation 47 | .post-nav { 48 | border-bottom: 1px solid $border-color; 49 | display: flex; 50 | float: left; 51 | width: 100%; 52 | 53 | .page-title, 54 | a { 55 | display: inline-block; 56 | } 57 | 58 | .page-title { 59 | font-size: 1.2rem; 60 | margin-bottom: 1rem; 61 | width: 100%; 62 | } 63 | 64 | i { 65 | vertical-align: middle; 66 | } 67 | } 68 | 69 | .center-image { 70 | display: block; 71 | margin: 0 auto; 72 | 73 | img { 74 | display: block; 75 | margin: 0 auto; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/client/epics/index.ts: -------------------------------------------------------------------------------- 1 | import { Action, Post, State } from 'common'; 2 | import { ActionsObservable, combineEpics, Epic } from 'redux-observable'; 3 | import * as Rx from 'rxjs'; 4 | 5 | import { 6 | errorSchedulePost, 7 | POSTS_REQUEST, 8 | SCHEDULE_POST_FAILURE, 9 | SCHEDULE_POST_REQUEST, 10 | SCHEDULE_POST_SUCCESS, 11 | 12 | SchedulePostFailureAction, 13 | SchedulePostRequestAction, 14 | 15 | setPosts, 16 | setScheduledPost, 17 | } from '../actions'; 18 | 19 | import { 20 | addPost, 21 | getPosts, 22 | } from '../services/posts'; 23 | 24 | type TEpic = Epic, State>; 25 | 26 | const getPostsEpic: TEpic = (action$: ActionsObservable>) => 27 | action$.ofType(POSTS_REQUEST) 28 | .mergeMap(() => ( 29 | Rx.Observable.fromPromise(getPosts()) 30 | .map((posts: Post[]) => setPosts(posts)) 31 | )); 32 | 33 | const addPostEpic: TEpic = ( 34 | action$: ActionsObservable>, 35 | ): Rx.Observable> => 36 | action$.ofType(SCHEDULE_POST_REQUEST) 37 | .mergeMap((action: SchedulePostRequestAction) => ( 38 | Rx.Observable.fromPromise(addPost(action.post)) 39 | .map((post: Post) => setScheduledPost(post)) 40 | .catch((error: any): Rx.Observable => ( 41 | Rx.Observable.of(errorSchedulePost(error)) 42 | )) 43 | )); 44 | 45 | export default combineEpics(getPostsEpic, addPostEpic); 46 | -------------------------------------------------------------------------------- /src/client/assets/scss/objects/_search-bar.scss: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | font-size: 1.4rem; 3 | position: relative; 4 | 5 | @media (min-width: 480px) { 6 | margin-right: 25px; 7 | } 8 | 9 | @media (max-width: 480px) { 10 | margin-bottom: 10px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | max-width: 120px; 14 | } 15 | 16 | &-label { 17 | font-size: 1.1rem; 18 | letter-spacing: .1em; 19 | text-transform: uppercase; 20 | } 21 | 22 | &-input { 23 | background: $brand-color; 24 | border: 0; 25 | border-bottom: 2px solid transparent; 26 | bottom: 0; 27 | cursor: pointer; 28 | display: block; 29 | max-width: 100%; 30 | min-width: 100%; 31 | opacity: 0; 32 | padding: 4px; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | transition: all .2s ease; 37 | z-index: 10; 38 | 39 | &:focus { 40 | border-bottom: 2px solid $brand-black; 41 | opacity: 1; 42 | outline: none; 43 | 44 | @media (min-width: 480px) { 45 | min-width: 120px; 46 | } 47 | 48 | @media (max-width: 480px) { 49 | left: -30px; 50 | max-width: 1000px; 51 | right: -30px; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .search-logo { 58 | font-size: 1.3rem; 59 | padding-bottom: 15px; 60 | padding-top: 15px; 61 | text-align: right; 62 | 63 | &-bg { 64 | // TODO: fix this 65 | // background: url('../images/Algolia_logo_bg-white.svg') no-repeat; 66 | background-size: contain; 67 | display: inline-block; 68 | height: 25px; 69 | margin-bottom: -10px; 70 | margin-left: 6px; 71 | width: 70px; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/client/actions/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-empty-interface 2 | import { Action, Post } from 'common'; 3 | 4 | export const POSTS_REQUEST = 'POSTS_REQUEST'; 5 | export const POSTS_SUCCESS = 'POSTS_SUCCESS'; 6 | export const POSTS_FAILURE = 'POSTS_FAILURE'; 7 | 8 | export interface PostsRequestAction extends Action {} 9 | export interface PostsSuccessAction extends Action { 10 | posts: Post[]; 11 | } 12 | export interface PostsFailureAction extends Action {} 13 | 14 | export const loadPosts = (): PostsRequestAction => ( 15 | { type: POSTS_REQUEST } 16 | ); 17 | 18 | export const setPosts = (posts: Post[]): PostsSuccessAction => { 19 | return { type: POSTS_SUCCESS, posts }; 20 | }; 21 | 22 | /** 23 | * Schedule Post 24 | */ 25 | export const SCHEDULE_POST_REQUEST = 'SCHEDULE_POST_REQUEST'; 26 | export const SCHEDULE_POST_SUCCESS = 'SCHEDULE_POST_SUCCESS'; 27 | export const SCHEDULE_POST_FAILURE = 'SCHEDULE_POST_FAILURE'; 28 | 29 | export interface SchedulePostRequestAction extends Action { 30 | post: Post; 31 | } 32 | export interface SchedulePostSuccessAction extends Action { 33 | post: Post; 34 | } 35 | export interface SchedulePostFailureAction extends Action { 36 | error: any; 37 | } 38 | 39 | export const requestSchedulePost = (post: Post): SchedulePostRequestAction => { 40 | return { type: SCHEDULE_POST_REQUEST, post }; 41 | }; 42 | export const setScheduledPost = (post: Post): SchedulePostSuccessAction => { 43 | return { type: SCHEDULE_POST_SUCCESS, post }; 44 | }; 45 | export const errorSchedulePost = (error: any): SchedulePostFailureAction => { 46 | return { type: SCHEDULE_POST_FAILURE, error }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/assets/scss/base/_utility.scss: -------------------------------------------------------------------------------- 1 | // Mix-ins 2 | 3 | %padding-small { 4 | padding: $padding-x-small $padding-x-small * 2; 5 | 6 | @media (max-width: 1000px) { 7 | padding: $padding-x-small; 8 | } 9 | } 10 | 11 | %padding-regular { 12 | padding: $padding-small $padding-large; 13 | 14 | @media (max-width: 1000px) { 15 | padding: $padding-small * 1.5 $padding-large / 1.6; 16 | } 17 | } 18 | 19 | %link-hover { 20 | font-family: $font-family-headings; 21 | line-height: 1.1; 22 | } 23 | 24 | %h1 { 25 | font-family: $font-family-headings; 26 | font-weight: lighter; 27 | line-height: 1.1; 28 | text-transform: uppercase; 29 | 30 | @media (min-width: 480px) { 31 | font-size: 5rem; 32 | } 33 | 34 | @media (max-width: 480px) { 35 | font-size: 3rem; 36 | } 37 | } 38 | 39 | %h2 { 40 | font-family: $font-family-headings; 41 | font-weight: 800; 42 | line-height: 1.1; 43 | text-transform: uppercase; 44 | 45 | @media (min-width: 480px) { 46 | font-size: 2.4rem; 47 | } 48 | 49 | @media (max-width: 480px) { 50 | font-size: 2rem; 51 | } 52 | } 53 | 54 | %h3 { 55 | background: $brand-yellow; 56 | display: inline-block; 57 | font-family: $font-family-headings; 58 | font-weight: lighter; 59 | line-height: 1.1; 60 | padding: .3em .5em; 61 | text-transform: uppercase; 62 | 63 | @media (min-width: 480px) { 64 | font-size: 1.6rem; 65 | } 66 | 67 | @media (max-width: 480px) { 68 | font-size: 1.3rem; 69 | } 70 | } 71 | 72 | %h4 { 73 | font-family: $font-family-headings; 74 | font-size: 1.8rem; 75 | line-height: 1.1; 76 | } 77 | // States 78 | .disabled { 79 | opacity: .7; 80 | } 81 | 82 | .visually-hidden { 83 | border: 0; 84 | clip: rect(0 0 0 0); 85 | height: 1px; 86 | margin: -1px; 87 | overflow: hidden; 88 | padding: 0; 89 | position: absolute; 90 | width: 1px; 91 | } 92 | -------------------------------------------------------------------------------- /src/client/assets/scss/includes/_header.scss: -------------------------------------------------------------------------------- 1 | .site-header { 2 | align-items: center; 3 | background: $brand-color; 4 | border-bottom: 1px solid $border-color; 5 | display: flex; 6 | flex-flow: row wrap; 7 | padding: 20px 45px; 8 | width: 100%; 9 | 10 | .branding-container { 11 | flex: auto; 12 | } 13 | 14 | .branding { 15 | display: inline-block; 16 | height: 60px; 17 | width: 160px; 18 | 19 | @media (max-width: 480px) { 20 | height: 40px; 21 | margin-bottom: 5px; 22 | width: 120px; 23 | } 24 | 25 | &:hover { 26 | background: none; 27 | text-decoration: none; 28 | } 29 | 30 | &::before { 31 | background: url('../images/logo_eleven_lab.svg') top left no-repeat; 32 | background-size: contain; 33 | content: ''; 34 | display: block; 35 | height: 60px; 36 | width: 160px; 37 | 38 | @media (max-width: 480px) { 39 | height: 40px; 40 | margin: 0 auto; 41 | width: 120px; 42 | } 43 | } 44 | } 45 | 46 | a { 47 | color: $header-link-color; 48 | } 49 | 50 | .avatar { 51 | border-radius: .2rem; 52 | float: left; 53 | height: 2rem; 54 | margin-right: 1rem; 55 | margin-top: -3px; 56 | width: 2rem; 57 | } 58 | 59 | .site-title { 60 | float: left; 61 | font-size: 1rem; 62 | font-weight: bold; 63 | line-height: 1.5; 64 | } 65 | 66 | .site-nav { 67 | ul { 68 | display: inline-block; 69 | line-height: 1.5; 70 | list-style: none; 71 | margin: 0; 72 | padding: 0; 73 | text-align: center; 74 | } 75 | 76 | li { 77 | display: inline-block; 78 | margin-right: .4rem; 79 | } 80 | } 81 | 82 | .branding-container, 83 | .search-bar, 84 | .site-nav { 85 | @media (max-width: 480px) { 86 | flex: 0 0 100%; 87 | text-align: center; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /definitions/react-infinite-calendar.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-infinite-calendar' { 2 | interface InfiniteCalendarLocale { 3 | locale?: any; 4 | blank?: string; 5 | headerFormat?: string; 6 | todayLabel?: { 7 | long?: string; 8 | short?: string; 9 | }; 10 | weekdays?: string[]; 11 | months?: string[]; 12 | weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; 13 | } 14 | 15 | interface InfiniteCalendarProps { 16 | autoFocus?: boolean; 17 | className?: string; 18 | DayComponent?: (props: any) => JSX.Element; 19 | disabledDates?: Date[]; 20 | disabledDays?: number[]; 21 | display?: 'years' | 'days'; 22 | displayOptions?: { 23 | hideYearsOnSelect?: boolean; 24 | layout?: 'portrait' | 'landscape'; 25 | overscanMonthCount?: number; 26 | shouldHeaderAnimate?: boolean; 27 | showHeader?: boolean; 28 | showMonthsForYears?: boolean; 29 | showOverlay?: boolean; 30 | showTodayHelper?: boolean; 31 | showWeekdays?: boolean; 32 | todayHelperRowOffset?: number; 33 | }; 34 | height?: number; 35 | keyboardSupport?: boolean; 36 | locale?: InfiniteCalendarLocale; 37 | max?: Date; 38 | maxDate?: Date; 39 | min?: Date; 40 | minDate?: Date; 41 | onScroll?: () => void; 42 | onScrollEnd?: () => void; 43 | onSelect?: () => void; 44 | rowHeight?: number; 45 | tabIndex?: number; 46 | theme?: { 47 | floatingNav?: { 48 | background?: string; 49 | chevron?: string; 50 | color?: string; 51 | }; 52 | headerColor?: string; 53 | selectionColor?: string | GetColor; 54 | textColor?: { 55 | active?: string; 56 | default?: string; 57 | }; 58 | todayColor?: string; 59 | weekdayColor?: string; 60 | }; 61 | width?: string | number; 62 | YearsComponent?: (props: any) => JSX.Element; 63 | } 64 | 65 | type GetColor = () => string; 66 | 67 | export default class InfiniteCalendar extends React.Component {} 68 | } 69 | -------------------------------------------------------------------------------- /src/client/assets/scss/base/_global.scss: -------------------------------------------------------------------------------- 1 | // scss-lint:disable VendorPrefix QualifyingElement 2 | * { 3 | box-sizing: border-box; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | figure { 9 | margin: 0; 10 | } 11 | // For correct line number width in Github Gists 12 | 13 | .gist td { 14 | box-sizing: content-box; 15 | } 16 | 17 | html { 18 | background: $background-color; 19 | font-size: 10px; 20 | } 21 | // Typography 22 | ::selection { 23 | background: $selection-color; 24 | } 25 | 26 | ::-moz-selection { 27 | background: $selection-color; 28 | } 29 | 30 | body { 31 | color: $brand-black; 32 | font-family: $font-family-headings; 33 | font-weight: lighter; 34 | word-wrap: break-word; 35 | 36 | @media (min-width: 480px) { 37 | font-size: $font-size; 38 | } 39 | 40 | @media (max-width: 480px) { 41 | font-size: $font-size-mobile; 42 | } 43 | } 44 | 45 | blockquote p:last-child, 46 | footer p:last-child { 47 | margin-bottom: 0; 48 | } 49 | 50 | table { 51 | table-layout: fixed; 52 | width: 100%; 53 | word-wrap: break-word; 54 | 55 | @media (max-width: 1100px) { 56 | display: inline-block; 57 | overflow-x: scroll; 58 | } 59 | } 60 | 61 | td, 62 | th { 63 | border: 1px solid $border-color; 64 | padding: .5em 1rem; 65 | text-align: left; 66 | } 67 | 68 | blockquote, 69 | code, 70 | dl, 71 | kbd, 72 | pre, 73 | samp, 74 | table { 75 | margin: 1em 0; 76 | } 77 | 78 | h1, 79 | h2, 80 | h3, 81 | h4 { 82 | margin: 0; 83 | } 84 | 85 | dt { 86 | font-weight: bold; 87 | } 88 | 89 | dd { 90 | margin-left: 2rem; 91 | } 92 | 93 | .math-display, 94 | dl, 95 | ol, 96 | p, 97 | ul { 98 | line-height: 1.5; 99 | margin-bottom: 1rem; 100 | } 101 | // KaTeX math display 102 | 103 | .math-display { 104 | display: inline-block; 105 | width: 100%; 106 | } 107 | // Lists within lists 108 | 109 | li > ol, 110 | li > ul { 111 | margin-bottom: 0; 112 | margin-left: 1rem; 113 | } 114 | 115 | ol, 116 | ul { 117 | list-style-position: inside; 118 | } 119 | 120 | hr { 121 | border: 0; 122 | border-bottom: 1px solid $brand-color; 123 | border-top: 1px solid $border-color; 124 | margin: 1em 0; 125 | } 126 | 127 | a:not(.no-link-style) { 128 | color: $brand-black; 129 | display: inline-block; 130 | letter-spacing: .05em; 131 | text-decoration: underline; 132 | 133 | &:hover { 134 | background: $brand-yellow; 135 | text-decoration: none; 136 | } 137 | 138 | &:visited, 139 | &:visited:hover { 140 | color: $button-hover; 141 | } 142 | } 143 | 144 | .nav { 145 | list-style: none; 146 | margin: 0; 147 | padding: 0; 148 | } 149 | // Responsive media 150 | 151 | embed, 152 | iframe, 153 | img, 154 | object, 155 | video { 156 | max-width: 100%; 157 | } 158 | 159 | img[align=left] { 160 | margin-right: 3%; 161 | } 162 | 163 | img[align=right] { 164 | margin-left: 3%; 165 | } 166 | -------------------------------------------------------------------------------- /src/server/api/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "posts": [ 3 | { 4 | "id": 1, 5 | "title": "lorem 1", 6 | "description": "Ut elit duis eiusmod excepteur ullamco non nisi eiusmod. Enim eiusmod ea Lorem do. Sunt sit nulla irure sit veniam quis consequat ipsum eiusmod aliquip ex anim amet. Voluptate laborum irure officia adipisicing.", 7 | "author": "6", 8 | "date": 1492385394915 9 | }, 10 | { 11 | "id": 2, 12 | "title": "lorem 2", 13 | "description": "Nisi aliqua aute consectetur Lorem incididunt ea et veniam. Sunt in anim sit veniam adipisicing aliqua anim ad reprehenderit aliquip irure. In ut consequat esse id ea officia nostrud dolor laboris laborum tempor aliqua enim. Ipsum occaecat id commodo magna Lorem.", 14 | "author": "5", 15 | "date": 1485417291586 16 | }, 17 | { 18 | "id": 3, 19 | "title": "lorem 3", 20 | "description": "Ipsum id commodo cupidatat consectetur reprehenderit elit qui culpa esse ea sit velit sit. Incididunt ullamco officia quis fugiat cupidatat sit consectetur excepteur in mollit voluptate sint. Veniam mollit reprehenderit reprehenderit incididunt cupidatat esse consequat exercitation dolor tempor proident reprehenderit ipsum. Enim enim elit commodo magna et anim in Lorem.", 21 | "author": "5", 22 | "date": 1484042789570 23 | }, 24 | { 25 | "id": 4, 26 | "title": "lorem 4", 27 | "description": "Aliqua ad excepteur sint ad. Pariatur qui sit in labore enim ea duis quis ipsum dolore Lorem. Veniam id ullamco ex dolor deserunt minim quis cillum quis nisi tempor id. Elit labore eu occaecat in non labore ut amet reprehenderit. Ipsum velit id non est dolore voluptate Lorem ea. Veniam magna esse sint anim pariatur laborum anim amet.", 28 | "author": "6", 29 | "date": 1500486920592 30 | }, 31 | { 32 | "id": 5, 33 | "title": "lorem 5", 34 | "description": "Incididunt minim excepteur duis officia ad. Cupidatat magna officia exercitation sit. Aute mollit elit mollit culpa commodo veniam. Lorem eiusmod laboris dolore officia cillum reprehenderit occaecat est cillum excepteur est eiusmod. Nostrud ullamco aute eiusmod et aliqua amet. Elit ut cillum anim magna.", 35 | "author": "5", 36 | "date": 1486225565744 37 | }, 38 | { 39 | "id": 6, 40 | "title": "lorem 6", 41 | "description": "Aliquip ea aliqua irure do cillum anim id proident. Enim Lorem sit labore consectetur velit eiusmod do. Ea dolore ad cupidatat nisi mollit eiusmod nulla ex culpa anim. Ullamco fugiat ullamco elit id fugiat ad tempor id magna cupidatat officia eu.", 42 | "author": "1", 43 | "date": 1488870724922 44 | }, 45 | { 46 | "id": 7, 47 | "title": "lorem 7", 48 | "description": "Minim Lorem reprehenderit tempor nisi commodo cupidatat nulla Lorem dolore ex veniam nostrud magna. Dolore culpa eiusmod ut id do minim aute do cupidatat Lorem. Nostrud velit exercitation ullamco proident ea eiusmod minim. Ex deserunt ipsum cupidatat qui qui occaecat consectetur sunt excepteur est magna commodo laboris. Adipisicing veniam dolor occaecat ea adipisicing. Est excepteur sint laborum dolore reprehenderit pariatur nulla incididunt.", 49 | "author": "6", 50 | "date": 1486077040491 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "statements" 7 | ], 8 | "array-type": [ 9 | true, 10 | "array" 11 | ], 12 | "class-name": true, 13 | "comment-format": [true, "check-space"], 14 | "jsdoc-format": true, 15 | "new-parens": true, 16 | "no-angle-bracket-type-assertion": true, 17 | "no-consecutive-blank-lines": [ 18 | true 19 | ], 20 | "object-literal-key-quotes": [ 21 | true, 22 | "consistent-as-needed" 23 | ], 24 | "object-literal-shorthand": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-finally", 29 | "check-else", 30 | "check-open-brace", 31 | "check-whitespace" 32 | ], 33 | "one-variable-per-declaration": [ 34 | true 35 | ], 36 | "ordered-imports": [ 37 | true 38 | ], 39 | "quotemark": [ 40 | true, 41 | "single", 42 | "jsx-double" 43 | ], 44 | "semicolon": [ 45 | true, 46 | "always" 47 | ], 48 | "variable-name": [ 49 | true, 50 | "check-format", 51 | "ban-keywords", 52 | "allow-pascal-case" 53 | ], 54 | "whitespace": [ 55 | true, 56 | "check-branch", 57 | "check-decl", 58 | "check-operator", 59 | "check-module", 60 | "check-separator", 61 | "check-type", 62 | "check-typecast" 63 | ], 64 | "eofline": true, 65 | "indent": [ 66 | true, 67 | "spaces" 68 | ], 69 | "linebreak-style": [ 70 | true, 71 | "LF" 72 | ], 73 | "max-line-length": [ 74 | true, 75 | 100 76 | ], 77 | "no-require-imports": true, 78 | "no-trailing-whitespace": true, 79 | "prefer-const": true, 80 | "trailing-comma": [ 81 | true, 82 | { 83 | "multiline": "always", 84 | "singleline": "never" 85 | } 86 | ], 87 | "adjacent-overload-signatures": true, 88 | "no-empty-interface": true, 89 | "no-inferrable-types": [ 90 | true 91 | ], 92 | "no-internal-module": true, 93 | "no-namespace": [true, "allow-declarations"], 94 | "no-reference": true, 95 | "no-var-requires": true, 96 | "only-arrow-functions": [ 97 | true, 98 | "allow-declarations", 99 | "allow-named-functions" 100 | ], 101 | "prefer-for-of": true, 102 | "typedef-whitespace": [ 103 | true, 104 | { 105 | "call-signature": "nospace", 106 | "index-signature": "nospace", 107 | "parameter": "nospace", 108 | "property-declaration": "nospace", 109 | "variable-declaration": "nospace" 110 | }, 111 | { 112 | "call-signature": "onespace", 113 | "index-signature": "onespace", 114 | "parameter": "onespace", 115 | "property-declaration": "onespace", 116 | "variable-declaration": "onespace" 117 | } 118 | ], 119 | "curly": true, 120 | "label-position": true, 121 | "no-arg": true, 122 | "no-conditional-assignment": true, 123 | "no-construct": true, 124 | "no-debugger": true, 125 | "no-duplicate-variable": true, 126 | "no-empty": true, 127 | "no-eval": true, 128 | "no-var-keyword": true, 129 | "triple-equals": [ 130 | true, 131 | "allow-null-check" 132 | ], 133 | "use-isnan": true 134 | } 135 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV.toLowerCase() : 'development'; 6 | const mode = NODE_ENV === 'development' ? 'dev' : 'dev'; 7 | 8 | 9 | // Create multiple instances 10 | const extractSCSS = new ExtractTextPlugin('app.css'); 11 | const extractCSS = new ExtractTextPlugin('vendor.css'); 12 | 13 | module.exports = { 14 | devtool: 'source-map', 15 | entry: { 16 | // styles: path.join(__dirname, 'src', 'client', 'assets', 'scss', 'main.scss'), 17 | // styles: path.join(__dirname, 'node_modules', 'react-dates', 'lib', 'css', '_datepicker.css'), 18 | main: path.join(__dirname, 'src', 'client', `index.${mode}`), 19 | }, 20 | plugins: [ 21 | extractCSS, 22 | extractSCSS, 23 | new webpack.optimize.CommonsChunkPlugin({ 24 | name: 'vendor', 25 | filename: 'vendor.js', 26 | minChunks(module) { 27 | const context = module.context; 28 | return context && context.indexOf('node_modules') >= 0; 29 | }, 30 | }), 31 | new webpack.IgnorePlugin(/\.\/locale$/), 32 | ], 33 | output: { 34 | path: path.join(__dirname, 'public'), 35 | filename: '[name].js', 36 | chunkFilename: '[name].js', 37 | publicPath: '/', 38 | library: '[name]', 39 | libraryTarget: 'umd', 40 | umdNamedDefine: true, 41 | }, 42 | resolve: { 43 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 44 | alias: { 45 | moment$: 'moment/moment.js', 46 | }, 47 | }, 48 | devServer: { 49 | historyApiFallback: true, 50 | contentBase: './public', 51 | port: 9001, 52 | watchOptions: { 53 | ignored: /node_modules/, 54 | aggregateTimeout: 300, 55 | poll: 1000, 56 | }, 57 | }, 58 | module: { 59 | rules: [ 60 | // less 61 | { 62 | test: /\.css$/, 63 | use: extractCSS.extract(['css-loader']), 64 | }, 65 | { 66 | test: /\.less$/, 67 | use: [ 68 | { loader: 'style-loader' }, 69 | { loader: 'css-loader' }, 70 | { loader: 'less-loader' }, 71 | ], 72 | }, 73 | { 74 | test: /\.scss$/, 75 | use: extractSCSS.extract(['css-loader', 'sass-loader']), 76 | }, 77 | // JS/TS 78 | // { test: /\.jsx?$/, use: [{ loader: 'babel-loader' }] }, 79 | { test: /\.tsx?$/, use: [{ loader: 'ts-loader' }] }, 80 | // images 81 | { 82 | test: /\.(jpe?g|png|gif)$/i, 83 | use: [{ 84 | loader: 'url-loader', 85 | options: { limit: 10240 }, 86 | }], 87 | }, 88 | // Font 89 | { 90 | test: /\.svg(\?[a-z0-9=&.]+)?$/, 91 | use: [ 92 | { 93 | loader: 'url-loader', 94 | options: { limit: 65000, mimetype: 'image/svg+xml' }, 95 | }, 96 | ], 97 | }, 98 | { 99 | test: /\.woff(\?[a-z0-9=&.]+)?$/, 100 | use: [{ 101 | loader: 'url-loader', 102 | options: { limit: 65000, mimetype: 'application/font-woff' }, 103 | }], 104 | }, 105 | { 106 | test: /\.woff2(\?[a-z0-9=&.]+)?$/, 107 | use: [{ 108 | loader: 'url-loader', 109 | options: { limit: 65000, mimetype: 'application/font-woff2' }, 110 | }], 111 | }, 112 | { 113 | test: /\.[ot]tf(\?[a-z0-9=&.]+)?$/, 114 | use: [{ 115 | loader: 'url-loader', 116 | options: { limit: 65000, mimetype: 'application/octet-stream' }, 117 | }], 118 | }, 119 | { 120 | test: /\.eot(\?[a-z0-9=&.]+)?$/, 121 | use: [{ 122 | loader: 'url-loader', 123 | options: { limit: 65000, mimetype: 'application/vnd.ms-fontobject' }, 124 | }], 125 | }, 126 | ], 127 | }, 128 | }; 129 | -------------------------------------------------------------------------------- /src/client/containers/Scheduler.tsx: -------------------------------------------------------------------------------- 1 | import { Post, State } from 'common'; 2 | import { History } from 'history'; 3 | import * as moment from 'moment'; 4 | import 'moment/locale/fr'; 5 | import * as React from 'react'; 6 | import InfiniteCalendar from 'react-infinite-calendar'; 7 | import { connect } from 'react-redux'; 8 | 9 | import { 10 | requestSchedulePost, 11 | SchedulePostRequestAction, 12 | } from '../actions'; 13 | import FormGroup from '../components/FormGroup'; 14 | 15 | moment.locale('fr'); 16 | 17 | interface Props { 18 | title?: string; 19 | description?: string; 20 | date?: number; 21 | history: History; 22 | } 23 | 24 | interface InternalState { 25 | title?: string; 26 | description?: string; 27 | date: moment.Moment; 28 | focused: boolean; 29 | } 30 | 31 | interface DispatchProps { 32 | requestSchedulePost: (post: Post) => SchedulePostRequestAction; 33 | } 34 | 35 | const mapStateToProps = ({ selectedPost }: State): State => ({ 36 | selectedPost, 37 | }); 38 | 39 | const mapDispatchToProps: DispatchProps = { 40 | requestSchedulePost, 41 | }; 42 | 43 | type AllProps = Readonly; 44 | 45 | const initialState: InternalState = { 46 | title: '', 47 | description: '', 48 | date: moment().add(1, 'day'), 49 | focused: false, 50 | }; 51 | 52 | class Scheduler extends React.Component { 53 | state = initialState; 54 | 55 | constructor(props: AllProps) { 56 | super(props); 57 | 58 | this.onTitleChange = this.onTitleChange.bind(this); 59 | this.onDescriptionChange = this.onDescriptionChange.bind(this); 60 | this.close = this.close.bind(this); 61 | } 62 | 63 | componentWillReceiveProps(nextProps: Props) { 64 | this.setState({ 65 | title: nextProps.title, 66 | description: nextProps.description, 67 | }); 68 | } 69 | 70 | onTitleChange(event: React.SyntheticEvent) { 71 | this.setState({ title: event.currentTarget.value }); 72 | } 73 | 74 | onDescriptionChange(event: React.SyntheticEvent) { 75 | this.setState({ description: event.currentTarget.value }); 76 | } 77 | 78 | close() { 79 | this.setState(initialState); 80 | this.props.history.push('/'); 81 | } 82 | 83 | render() { 84 | // const fields = ['_blank', '_headerFormat', '_todayLabel', '_weekdays', '_weekStartsOn']; 85 | // const rawLocale = moment.localeData('fr'); 86 | // const locale = Object.keys(rawLocale).reduce((acc: InfiniteCalendarLocale, key: string) => { 87 | // return acc; 88 | // }, {} as InfiniteCalendarLocale); 89 | 90 | const { title = '', description = '' } = this.state; 91 | 92 | return ( 93 |
94 |

Planifier un article

95 |
96 |
97 | 104 | 105 | 111 |