= props =>
5 |
6 | {props.name}
7 |
8 |
9 | SSR.getInitialProps = async (context) => {
10 | const props = {
11 | name: '"next.js-typescript-starter-kit" from client'
12 | }
13 | const server = !!context.req
14 |
15 | if (server) {
16 | props.name = await mockFetchName()
17 | }
18 |
19 | return props
20 | }
21 | export default SSR
22 |
23 | interface Props {
24 | name
25 | }
26 | async function mockFetchName() {
27 | return '"next.js-typescript-starter-kit" from server'
28 | }
29 |
--------------------------------------------------------------------------------
/pages/StyledJsx.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {Layout} from '../src/components/Layout'
3 |
4 | export default props =>
5 |
6 |
7 | {/*language=PostCSS*/}
8 |
14 | StyledJsx
15 |
16 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch'
2 | import * as React from 'react'
3 | import {Provider} from 'react-redux'
4 | import App, {Container} from 'next/app'
5 | import {getStore} from '../src/store'
6 |
7 | export default class extends App {
8 | static async getInitialProps({Component, router, ctx}) {
9 | const server = !!ctx.req
10 | const store = getStore(undefined, server)
11 | const state = store.getState()
12 | const out = {state, server} as any
13 |
14 | if (Component.getInitialProps) {
15 | return {
16 | ...out,
17 | pageProps: {
18 | ...await Component.getInitialProps(ctx)
19 | }
20 | }
21 | }
22 |
23 | return out
24 | }
25 |
26 | render() {
27 | const {props} = this as any
28 | const {Component, pageProps} = props
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Document, {Head, Main, NextScript} from 'next/document'
3 | import {
4 | DEV, FB_TRACKING_ID, SENTRY_TRACKING_ID, SITE_DESCRIPTION, SITE_IMAGE,
5 | SITE_NAME, SITE_TITLE
6 | } from '../src/constants/env'
7 |
8 | export default class extends Document {
9 | static async getInitialProps(...args) {
10 | const documentProps = await Document.getInitialProps(...args)
11 | const {req, renderPage} = args[0]
12 | const page = renderPage()
13 |
14 | return {...documentProps, ...page}
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
43 |
44 |
45 |
46 | {!DEV && FB_TRACKING_ID && (
47 |
55 | )}
56 | {!DEV && FB_TRACKING_ID && (
57 |
64 | )}
65 | {!DEV && SENTRY_TRACKING_ID && (
66 | <>
67 |
71 |
74 | >
75 | )}
76 |
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {Home} from '../src/components/Home'
3 | import {Layout} from '../src/components/Layout'
4 |
5 | export default props =>
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-preset-env')()
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/@types/global.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | __REDUX_DEVTOOLS_EXTENSION__: any
3 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any
4 | __REACT_DEVTOOLS_GLOBAL_HOOK__: any
5 | }
6 | declare module '*.png' {
7 | const resource: string
8 | export = resource
9 | }
10 | declare module '*.svg' {
11 | const resource: string
12 | export = resource
13 | }
14 | declare module '*.css' {
15 | const resource: any
16 | export = resource
17 | }
18 | declare module '*.pcss' {
19 | const resource: string
20 | export = resource
21 | }
22 | declare module '*.json' {
23 | const resource: any
24 | export = resource
25 | }
26 |
--------------------------------------------------------------------------------
/src/@types/react.d.ts:
--------------------------------------------------------------------------------
1 | import {FunctionComponent} from 'react'
2 |
3 | declare module 'react' {
4 | export interface NextFunctionComponent extends FunctionComponent
{
5 | getInitialProps(ctx): Promise
6 | }
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/@types/styled-jsx-css.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'styled-jsx/css' {
2 | const css: any
3 | export default css
4 | }
5 |
--------------------------------------------------------------------------------
/src/@types/styled-jsx.d.ts:
--------------------------------------------------------------------------------
1 | import 'react';
2 |
3 | declare module 'react' {
4 | interface StyleHTMLAttributes extends React.HTMLAttributes {
5 | jsx?: boolean;
6 | global?: boolean;
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/src/components/Color.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --colorPrimary: #0057AC;
3 | --colorSecondary: #FEDF00;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const Footer: React.FunctionComponent = props =>
4 |
7 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {Nav} from './Nav'
3 |
4 | export const Header: React.FunctionComponent = props =>
5 |
6 |
7 | header
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/Home.css:
--------------------------------------------------------------------------------
1 | @import 'Color.css';
2 |
3 | .home {
4 | color: var(--colorPrimary);
5 | background: var(--colorSecondary);
6 | div {
7 | background: var(--colorPrimary);
8 | color: var(--colorSecondary);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/components/Home.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {shallow} from 'enzyme'
3 |
4 | import {Home} from './Home'
5 |
6 | describe('Component', () => {
7 | describe('Home', () => {
8 | it('should render without throwing an error', function () {
9 | expect(shallow().contains(
10 |
11 | usage classnames in Home.tsx
12 |
13 | )).toEqual(true)
14 | })
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/components/Home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as classnames from 'classnames'
3 | import * as css from './Home.css'
4 |
5 | export const Home: React.FunctionComponent = props =>
6 |
7 |
8 | -
9 | usage classnames in Home.tsx
10 |
11 | -
12 | Layout.tsx set background-color hot-pink using global styled jsx
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {Header} from './Header'
3 | import {Footer} from './Footer'
4 | import Head from 'next/head'
5 |
6 | export const Layout: React.FunctionComponent = props =>
7 |
8 | {/*language=PostCSS*/}
9 |
15 |
16 |
20 |
21 |
22 |
23 | {props.children}
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/Nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Link from 'next/link'
3 |
4 | export const Nav: React.FunctionComponent = props =>
5 |
32 |
--------------------------------------------------------------------------------
/src/constants/env.ts:
--------------------------------------------------------------------------------
1 | export const DEV = process.env.NODE_ENV !== 'production'
2 |
3 | export const GA_TRACKING_ID = ''
4 | export const FB_TRACKING_ID = ''
5 | export const SENTRY_TRACKING_ID = ''
6 |
7 | export const SITE_NAME = ''
8 | export const SITE_TITLE = ''
9 | export const SITE_DESCRIPTION = ''
10 | export const SITE_IMAGE = ''
11 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export function cleanObject(object: object, valueKeeper?: any) {
2 | return Object
3 | .entries(object)
4 | .reduce((ret, [key, value]) => {
5 | if (value === undefined || value === null) {
6 | if (typeof valueKeeper !== 'undefined') {
7 | ret[key] = valueKeeper
8 | }
9 | return ret
10 | }
11 | if (!Array.isArray(value) && typeof value === 'object') {
12 | ret[key] = cleanObject(value)
13 | } else {
14 | ret[key] = value
15 | }
16 | return ret
17 | }, {})
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/timer.ts:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment'
2 |
3 | export default class Timer {
4 | private _time;
5 | private _timer;
6 | private _initialSeconds;
7 | private _listener: (remains: number) => void;
8 | private _timeoutListener: () => void;
9 |
10 | constructor(seconds: number, listener, timeoutListener?) {
11 | this._initialSeconds = seconds;
12 | this._listener = listener;
13 | this._timeoutListener = timeoutListener;
14 | this.reset();
15 | }
16 |
17 | start() {
18 | this._timer = setInterval(_ => {
19 | this._time.add(-1000);
20 | this._listener(this._time);
21 | if (this._time.valueOf() <= 0) {
22 | if (this._timeoutListener) {
23 | this._timeoutListener();
24 | }
25 | this.end();
26 | }
27 | }, 1000);
28 | }
29 |
30 | end() {
31 | clearInterval(this._timer);
32 | this.reset();
33 | }
34 |
35 | reset() {
36 | this._time = moment(this._initialSeconds);
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/src/redux/common.ts:
--------------------------------------------------------------------------------
1 | export function createActions(actionType: string): [string, string, string] {
2 | return [`request ${actionType}`, `success ${actionType}`, `fail ${actionType}`]
3 | }
4 |
5 | export const REQUEST: REQUEST = 0
6 | export const SUCCESS: SUCCESS = 1
7 | export const FAIL: FAIL = 2
8 |
9 | export type REQUEST = 0
10 | export type SUCCESS = 1
11 | export type FAIL = 2
12 |
--------------------------------------------------------------------------------
/src/redux/index.ts:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux'
2 | import {reducer as persist, PersistState} from './persist'
3 | import {reducer as system, SystemState} from './system'
4 |
5 | export const reducer = combineReducers({
6 | persist,
7 | system,
8 | })
9 |
10 | export interface RootState {
11 | persist: PersistState,
12 | system: SystemState,
13 | }
14 |
--------------------------------------------------------------------------------
/src/redux/log/index.ts:
--------------------------------------------------------------------------------
1 | import * as GA from 'react-ga'
2 | import {GA_TRACKING_ID} from '../../constants/env'
3 | import {DEV} from '../../constants/env'
4 | import Router from 'next/router'
5 | import * as qs from 'querystring'
6 | import {addRouteCompleteEvent} from '../router'
7 |
8 | const INITIALIZED_GA = 'INITIALIZED_GA'
9 | const SKIP_GA = 'SKIP_GA'
10 |
11 | export const initializeGa = (userId?) => {
12 | if (!DEV && GA_TRACKING_ID) {
13 | const gaOptions = userId ? {userId} : {}
14 |
15 | GA.initialize(GA_TRACKING_ID, {debug: DEV, gaOptions})
16 | GA.pageview([Router.pathname, qs.stringify(Router.query)].join('?'))
17 |
18 | addRouteCompleteEvent(url => GA.pageview(url))
19 |
20 | return {type: INITIALIZED_GA}
21 | }
22 | return {type: SKIP_GA}
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/redux/persist/index.ts:
--------------------------------------------------------------------------------
1 | import {Reducer} from 'redux'
2 |
3 | const defaultState = {} as PersistState
4 | export const reducer: Reducer = (state = defaultState, action) => {
5 | const {type, payload} = action
6 | switch (type) {
7 | case ActionTypes.SAVE_USER_INFO:
8 | return {
9 | ...state,
10 | userInfo: payload
11 | }
12 | default:
13 | return state
14 | }
15 | }
16 |
17 | enum ActionTypes {
18 | SAVE_USER_INFO = 'save user information'
19 | }
20 |
21 | export function saveUserInfo(userInfo) {
22 | return {
23 | type: ActionTypes.SAVE_USER_INFO,
24 | payload: userInfo
25 | }
26 | }
27 |
28 | export interface PersistState {
29 | userInfo: any
30 | }
31 |
--------------------------------------------------------------------------------
/src/redux/router/index.ts:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 |
3 | export function addRouteCompleteEvent(fx: (url) => void) {
4 | onRouteCompleteEvents.push(fx)
5 | }
6 |
7 | const onRouteCompleteEvents = []
8 |
9 | Router.onRouteChangeComplete = (url) => {
10 | onRouteCompleteEvents.forEach(fx => fx(url))
11 | }
12 |
--------------------------------------------------------------------------------
/src/redux/system/index.ts:
--------------------------------------------------------------------------------
1 | import {REHYDRATE} from 'redux-persist/constants'
2 | import {createActions, FAIL, REQUEST, SUCCESS} from '../common'
3 | import {initializeGa} from '../log/index'
4 | import {saveUserInfo} from '../persist/index'
5 | import {Reducer} from 'redux'
6 |
7 | const defaultState = {} as SystemState
8 |
9 | export const reducer: Reducer = (state = defaultState, action) => {
10 | const {type, payload} = action
11 | switch (type) {
12 | case ActionTypes.BOOT:
13 | return {
14 | ...state,
15 | boot: true
16 | }
17 | case REHYDRATE:
18 | return {
19 | ...state,
20 | reHydrated: true
21 | }
22 | default:
23 | return state
24 | }
25 | }
26 |
27 | enum ActionTypes {
28 | BOOT = 'boot',
29 | SESSION = 'session',
30 | }
31 |
32 | export function boot() {
33 | return {
34 | type: ActionTypes.BOOT
35 | }
36 | }
37 |
38 | const SESSION = createActions(ActionTypes.SESSION)
39 |
40 | export function session() {
41 | return async (dispatch, getState) => {
42 | dispatch({type: SESSION[REQUEST]})
43 | try {
44 | const userInfo: any = await (async _ => {
45 | throw new Error('write session manging code')
46 | })()
47 | dispatch({type: SESSION[SUCCESS]})
48 | dispatch(saveUserInfo(userInfo))
49 | dispatch(initializeGa(userInfo.id))
50 | } catch (ex) {
51 | dispatch({type: SESSION[FAIL]})
52 | dispatch(initializeGa())
53 | } finally {
54 | dispatch(boot())
55 | }
56 | }
57 | }
58 |
59 | export interface SystemState {
60 | boot: boolean
61 | reHydrated: boolean
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import {applyMiddleware, compose, createStore, Store} from 'redux'
2 | import {reducer, RootState} from './redux'
3 | import {DEV} from './constants/env'
4 | import thunk from 'redux-thunk'
5 | import {createLogger} from 'redux-logger';
6 | import {persistStore, autoRehydrate} from 'redux-persist'
7 | import {session} from './redux/system'
8 |
9 | let store
10 |
11 | export const getStore = (state, isServer?): Store => {
12 | if (isServer && typeof window === 'undefined') {
13 | return createStore(reducer, state, applyMiddleware(thunk))
14 | } else {
15 | const composeEnhancers = DEV && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
16 |
17 | if (!store) {
18 | const mw = [thunk]
19 | if (!DEV) {
20 | if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
21 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {}
22 | }
23 | } else {
24 | mw.push(createLogger({
25 | predicate: (getState, action) => !/^@@/.test(action.type),
26 | collapsed: true
27 | }));
28 | }
29 |
30 | store = createStore(
31 | reducer,
32 | state,
33 | composeEnhancers(applyMiddleware(...mw), autoRehydrate())
34 | )
35 | store.dispatch(session());
36 |
37 | const whitelist = ['persist']
38 | persistStore(store, {whitelist}, _ => {
39 | console.log(`define whitelist: ${whitelist.join(', ')}`)
40 | })
41 | }
42 | return store
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deptno/next.js-typescript-starter-kit/f3883099d9d860d8164ed1a0d2e6cf4a9af8bd96/static/favicon.ico
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "target": "esnext",
5 | "jsx": "preserve",
6 | "sourceMap": false,
7 | "moduleResolution": "node"
8 | },
9 | "exclude": [
10 | "out",
11 | ".next"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "semicolon": [false, "never"],
4 | "quotemark": [true, "single", "jsx-double"],
5 | "indent": [true, "spaces", 2]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------